feat: implement complete CMT backend with API endpoints and test suite
- Add 7 core API endpoints: users, transactions, partners, products, inventory, payments, credit - Implement role-based authentication (admin/write/read-only access) - Add comprehensive database models with proper relationships - Include full test coverage for all endpoints and business logic - Set up Alembic migrations and Docker configuration - Configure FastAPI app with CORS and database integration
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from app.schemas.base import PartnerType
|
||||
|
||||
|
||||
class TestPartnerCreation:
|
||||
"""Test partner creation endpoints."""
|
||||
|
||||
def test_create_partner_with_admin_access(self, client: TestClient, admin_token: str):
|
||||
"""Test partner creation with admin token."""
|
||||
partner_data = {
|
||||
"tin_number": 987654321,
|
||||
"names": "New Test Partner",
|
||||
"type": PartnerType.CLIENT,
|
||||
"phone_number": "0987654321"
|
||||
}
|
||||
response = client.post("/api/v1/partners/",
|
||||
json=partner_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["tin_number"] == 987654321
|
||||
assert data["names"] == "New Test Partner"
|
||||
assert data["type"] == PartnerType.CLIENT
|
||||
assert data["phone_number"] == "0987654321"
|
||||
assert "id" in data
|
||||
|
||||
def test_create_partner_with_write_access(self, client: TestClient, write_token: str):
|
||||
"""Test partner creation with write token."""
|
||||
partner_data = {
|
||||
"tin_number": 555666777,
|
||||
"names": "Write Access Partner",
|
||||
"type": PartnerType.SUPPLIER,
|
||||
"phone_number": "0555666777"
|
||||
}
|
||||
response = client.post("/api/v1/partners/",
|
||||
json=partner_data,
|
||||
headers={"Authorization": f"Bearer {write_token}"})
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["tin_number"] == 555666777
|
||||
assert data["type"] == PartnerType.SUPPLIER
|
||||
|
||||
def test_create_partner_without_phone(self, client: TestClient, admin_token: str):
|
||||
"""Test partner creation without phone number."""
|
||||
partner_data = {
|
||||
"tin_number": 111222333,
|
||||
"names": "Partner Without Phone",
|
||||
"type": PartnerType.CLIENT
|
||||
}
|
||||
response = client.post("/api/v1/partners/",
|
||||
json=partner_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["tin_number"] == 111222333
|
||||
|
||||
def test_create_partner_unauthorized(self, client: TestClient):
|
||||
"""Test partner creation without authentication."""
|
||||
partner_data = {
|
||||
"tin_number": 444555666,
|
||||
"names": "Unauthorized Partner",
|
||||
"type": PartnerType.CLIENT
|
||||
}
|
||||
response = client.post("/api/v1/partners/", json=partner_data)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_create_partner_read_only_forbidden(self, client: TestClient, read_only_token: str):
|
||||
"""Test partner creation with read-only access should fail."""
|
||||
partner_data = {
|
||||
"tin_number": 777888999,
|
||||
"names": "Read Only Attempt",
|
||||
"type": PartnerType.CLIENT
|
||||
}
|
||||
response = client.post("/api/v1/partners/",
|
||||
json=partner_data,
|
||||
headers={"Authorization": f"Bearer {read_only_token}"})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_create_partner_duplicate_tin(self, client: TestClient, admin_token: str, sample_partner):
|
||||
"""Test creation with duplicate TIN number should fail."""
|
||||
partner_data = {
|
||||
"tin_number": sample_partner.tin_number, # Duplicate TIN
|
||||
"names": "Duplicate TIN Partner",
|
||||
"type": PartnerType.SUPPLIER
|
||||
}
|
||||
response = client.post("/api/v1/partners/",
|
||||
json=partner_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 400
|
||||
assert "TIN number already exists" in response.json()["detail"]
|
||||
|
||||
|
||||
class TestPartnerRetrieval:
|
||||
"""Test partner retrieval endpoints."""
|
||||
|
||||
def test_get_all_partners_with_auth(self, client: TestClient, admin_token: str, multiple_partners):
|
||||
"""Test retrieving all partners with authentication."""
|
||||
response = client.get("/api/v1/partners/",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 3 # At least the fixture partners
|
||||
|
||||
def test_get_all_partners_read_only_access(self, client: TestClient, read_only_token: str, multiple_partners):
|
||||
"""Test read-only user can retrieve partners."""
|
||||
response = client.get("/api/v1/partners/",
|
||||
headers={"Authorization": f"Bearer {read_only_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_get_partners_unauthorized(self, client: TestClient):
|
||||
"""Test retrieving partners without authentication."""
|
||||
response = client.get("/api/v1/partners/")
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_get_partners_with_pagination(self, client: TestClient, admin_token: str, multiple_partners):
|
||||
"""Test partner retrieval with pagination."""
|
||||
response = client.get("/api/v1/partners/?skip=0&limit=2",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) <= 2
|
||||
|
||||
def test_get_single_partner_by_id(self, client: TestClient, admin_token: str, sample_partner):
|
||||
"""Test retrieving a single partner by ID."""
|
||||
response = client.get(f"/api/v1/partners/{sample_partner.id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == sample_partner.id
|
||||
assert data["tin_number"] == sample_partner.tin_number
|
||||
assert data["names"] == sample_partner.names
|
||||
|
||||
def test_get_nonexistent_partner(self, client: TestClient, admin_token: str):
|
||||
"""Test retrieving a non-existent partner."""
|
||||
response = client.get("/api/v1/partners/99999",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 404
|
||||
assert "Partner not found" in response.json()["detail"]
|
||||
|
||||
|
||||
class TestPartnerUpdate:
|
||||
"""Test partner update endpoints."""
|
||||
|
||||
def test_update_partner_with_write_access(self, client: TestClient, write_token: str, sample_partner):
|
||||
"""Test updating partner with write access."""
|
||||
update_data = {
|
||||
"names": "Updated Partner Name",
|
||||
"type": PartnerType.SUPPLIER
|
||||
}
|
||||
response = client.put(f"/api/v1/partners/{sample_partner.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {write_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["names"] == "Updated Partner Name"
|
||||
assert data["type"] == PartnerType.SUPPLIER
|
||||
assert data["tin_number"] == sample_partner.tin_number # Unchanged
|
||||
|
||||
def test_update_partner_tin_number(self, client: TestClient, admin_token: str, sample_partner):
|
||||
"""Test updating partner TIN number."""
|
||||
update_data = {
|
||||
"tin_number": 999888777
|
||||
}
|
||||
response = client.put(f"/api/v1/partners/{sample_partner.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["tin_number"] == 999888777
|
||||
|
||||
def test_update_partner_read_only_forbidden(self, client: TestClient, read_only_token: str, sample_partner):
|
||||
"""Test updating partner with read-only access should fail."""
|
||||
update_data = {
|
||||
"names": "Should Not Update"
|
||||
}
|
||||
response = client.put(f"/api/v1/partners/{sample_partner.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {read_only_token}"})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_update_partner_duplicate_tin(self, client: TestClient, admin_token: str, multiple_partners):
|
||||
"""Test updating partner with duplicate TIN should fail."""
|
||||
partner_to_update = multiple_partners[0]
|
||||
existing_tin = multiple_partners[1].tin_number
|
||||
|
||||
update_data = {
|
||||
"tin_number": existing_tin
|
||||
}
|
||||
response = client.put(f"/api/v1/partners/{partner_to_update.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 400
|
||||
assert "TIN number already exists" in response.json()["detail"]
|
||||
|
||||
def test_update_nonexistent_partner(self, client: TestClient, admin_token: str):
|
||||
"""Test updating a non-existent partner."""
|
||||
update_data = {
|
||||
"names": "Non-existent Partner"
|
||||
}
|
||||
response = client.put("/api/v1/partners/99999",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 404
|
||||
assert "Partner not found" in response.json()["detail"]
|
||||
|
||||
|
||||
class TestPartnerDeletion:
|
||||
"""Test partner deletion endpoints."""
|
||||
|
||||
def test_delete_partner_with_admin_access(self, client: TestClient, admin_token: str, sample_partner):
|
||||
"""Test deleting partner with admin access."""
|
||||
response = client.delete(f"/api/v1/partners/{sample_partner.id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 204
|
||||
|
||||
# Verify partner is deleted
|
||||
get_response = client.get(f"/api/v1/partners/{sample_partner.id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert get_response.status_code == 404
|
||||
|
||||
def test_delete_partner_write_access_forbidden(self, client: TestClient, write_token: str, sample_partner):
|
||||
"""Test deleting partner with write access should fail."""
|
||||
response = client.delete(f"/api/v1/partners/{sample_partner.id}",
|
||||
headers={"Authorization": f"Bearer {write_token}"})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_delete_partner_read_only_forbidden(self, client: TestClient, read_only_token: str, sample_partner):
|
||||
"""Test deleting partner with read-only access should fail."""
|
||||
response = client.delete(f"/api/v1/partners/{sample_partner.id}",
|
||||
headers={"Authorization": f"Bearer {read_only_token}"})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_delete_nonexistent_partner(self, client: TestClient, admin_token: str):
|
||||
"""Test deleting a non-existent partner."""
|
||||
response = client.delete("/api/v1/partners/99999",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 404
|
||||
assert "Partner not found" in response.json()["detail"]
|
||||
|
||||
def test_delete_partner_unauthorized(self, client: TestClient, sample_partner):
|
||||
"""Test deleting partner without authentication."""
|
||||
response = client.delete(f"/api/v1/partners/{sample_partner.id}")
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
class TestPartnerValidation:
|
||||
"""Test partner data validation."""
|
||||
|
||||
def test_create_partner_invalid_data(self, client: TestClient, admin_token: str):
|
||||
"""Test creating partner with invalid data."""
|
||||
# Missing required field
|
||||
partner_data = {
|
||||
"names": "Missing TIN Partner",
|
||||
"type": PartnerType.CLIENT
|
||||
}
|
||||
response = client.post("/api/v1/partners/",
|
||||
json=partner_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
def test_create_partner_invalid_type(self, client: TestClient, admin_token: str):
|
||||
"""Test creating partner with invalid type."""
|
||||
partner_data = {
|
||||
"tin_number": 123456789,
|
||||
"names": "Invalid Type Partner",
|
||||
"type": "INVALID_TYPE"
|
||||
}
|
||||
response = client.post("/api/v1/partners/",
|
||||
json=partner_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 422 # Validation error
|
||||
Reference in New Issue
Block a user