c086f64363
- 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
377 lines
17 KiB
Python
377 lines
17 KiB
Python
"""Integration tests for API endpoints with database interactions."""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlmodel import Session, select
|
|
|
|
from app.schemas.models import User, Partner, Product, Transaction, Credit, Inventory, Payment
|
|
from app.schemas.base import UserRole, PartnerType, TransactionType, TransactionStatus, PaymentMethod
|
|
|
|
|
|
class TestUserAPIIntegration:
|
|
"""Test User API endpoints with database integration."""
|
|
|
|
def test_create_user_endpoint_with_database(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test creating a user through API endpoint and verifying database storage."""
|
|
user_data = {
|
|
"username": "api_test_user",
|
|
"password": "test_password",
|
|
"role": "READ_ONLY"
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/users/", json=user_data, headers=integration_admin_token)
|
|
assert response.status_code == 201
|
|
|
|
created_user = response.json()
|
|
assert created_user["username"] == "api_test_user"
|
|
assert created_user["role"] == "read_only"
|
|
assert "id" in created_user
|
|
|
|
def test_get_user_endpoint_with_database(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test retrieving a user through API endpoint from database."""
|
|
# Create user directly in database
|
|
user = User(username="db_user", password_hash="hashed", role=UserRole.ADMIN)
|
|
integration_session.add(user)
|
|
integration_session.commit()
|
|
integration_session.refresh(user)
|
|
|
|
# Retrieve through API
|
|
response = integration_client.get(f"/api/v1/users/{user.id}", headers=integration_admin_token)
|
|
assert response.status_code == 200
|
|
|
|
returned_user = response.json()
|
|
assert returned_user["username"] == "db_user"
|
|
assert returned_user["role"] == "admin"
|
|
|
|
def test_update_user_endpoint_with_database(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test updating a user through API endpoint and verifying database changes."""
|
|
# Create user directly in database
|
|
user = User(username="update_user", password_hash="hashed", role=UserRole.READ_ONLY)
|
|
integration_session.add(user)
|
|
integration_session.commit()
|
|
integration_session.refresh(user)
|
|
|
|
# Update through API
|
|
update_data = {"role": "write"}
|
|
response = integration_client.put(f"/api/v1/users/{user.id}", json=update_data, headers=integration_admin_token)
|
|
assert response.status_code == 200
|
|
|
|
# Verify in database
|
|
integration_session.refresh(user)
|
|
assert user.role == UserRole.WRITE
|
|
|
|
def test_delete_user_endpoint_with_database(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test deleting a user through API endpoint and verifying database removal."""
|
|
# Create user directly in database
|
|
user = User(username="delete_user", password_hash="hashed", role=UserRole.READ_ONLY)
|
|
integration_session.add(user)
|
|
integration_session.commit()
|
|
user_id = user.id
|
|
|
|
# Delete through API
|
|
response = integration_client.delete(f"/api/v1/users/{user_id}", headers=integration_admin_token)
|
|
assert response.status_code == 200
|
|
|
|
# Verify removed from database
|
|
deleted_user = integration_session.get(User, user_id)
|
|
assert deleted_user is None
|
|
|
|
|
|
class TestPartnerAPIIntegration:
|
|
"""Test Partner API endpoints with database integration."""
|
|
|
|
def test_create_partner_endpoint_with_database(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test creating a partner through API endpoint and verifying database storage."""
|
|
partner_data = {
|
|
"tin_number": 123456789,
|
|
"names": "Test Partner Co.",
|
|
"type": "SUPPLIER",
|
|
"phone_number": "1234567890"
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/partners/", json=partner_data, headers=integration_admin_token)
|
|
assert response.status_code == 201
|
|
|
|
created_partner = response.json()
|
|
assert created_partner["tin_number"] == 123456789
|
|
assert created_partner["names"] == "Test Partner Co."
|
|
assert created_partner["type"] == "SUPPLIER"
|
|
|
|
def test_get_partners_endpoint_with_database(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test retrieving partners through API endpoint from database."""
|
|
# Create partner directly in database
|
|
partner = Partner(
|
|
tin_number=987654321,
|
|
names="DB Partner",
|
|
type=PartnerType.CLIENT,
|
|
phone_number="9876543210"
|
|
)
|
|
integration_session.add(partner)
|
|
integration_session.commit()
|
|
|
|
# Retrieve through API
|
|
response = integration_client.get("/api/v1/partners/", headers=integration_admin_token)
|
|
assert response.status_code == 200
|
|
|
|
partners = response.json()
|
|
assert len(partners) >= 1
|
|
partner_names = [p["names"] for p in partners]
|
|
assert "DB Partner" in partner_names
|
|
|
|
def test_partner_unique_constraint_through_api(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test partner unique constraint enforcement through API."""
|
|
# Create partner directly in database
|
|
partner = Partner(
|
|
tin_number=999999999,
|
|
names="Unique Partner",
|
|
type=PartnerType.CLIENT,
|
|
phone_number="5555555555"
|
|
)
|
|
integration_session.add(partner)
|
|
integration_session.commit()
|
|
|
|
# Try to create duplicate through API
|
|
duplicate_data = {
|
|
"tin_number": 999999999,
|
|
"names": "Different Name",
|
|
"type": "SUPPLIER",
|
|
"phone_number": "8888888888"
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/partners/", json=duplicate_data, headers=integration_admin_token)
|
|
assert response.status_code == 400 # Should fail due to unique constraint
|
|
|
|
|
|
class TestTransactionAPIIntegration:
|
|
"""Test Transaction API endpoints with database integration."""
|
|
|
|
def test_create_transaction_with_valid_relationships(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test creating a transaction through API with valid partner and user relationships."""
|
|
# Create required entities in database
|
|
partner = Partner(tin_number=111111111, names="Trans Partner", type=PartnerType.CLIENT, phone_number="1111111111")
|
|
user = User(username="trans_user", password_hash="hashed", role=UserRole.WRITE)
|
|
integration_session.add(partner)
|
|
integration_session.add(user)
|
|
integration_session.commit()
|
|
integration_session.refresh(partner)
|
|
integration_session.refresh(user)
|
|
|
|
transaction_data = {
|
|
"amount": 1000.50,
|
|
"transaction_type": "SALE",
|
|
"status": "COMPLETED",
|
|
"partner_id": partner.id,
|
|
"user_id": user.id
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/transactions/", json=transaction_data, headers=integration_admin_token)
|
|
assert response.status_code == 201
|
|
|
|
created_transaction = response.json()
|
|
assert created_transaction["amount"] == 1000.50
|
|
assert created_transaction["partner_id"] == partner.id
|
|
|
|
def test_create_transaction_with_invalid_partner(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test creating a transaction with invalid partner ID through API."""
|
|
transaction_data = {
|
|
"amount": 500.00,
|
|
"transaction_type": "PURCHASE",
|
|
"status": "PENDING",
|
|
"partner_id": 99999, # Invalid partner ID
|
|
"user_id": 1
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/transactions/", json=transaction_data, headers=integration_admin_token)
|
|
assert response.status_code == 400 # Should fail due to foreign key constraint
|
|
|
|
def test_get_transactions_by_partner(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test retrieving transactions filtered by partner through API."""
|
|
# Create test data
|
|
partner1 = Partner(tin_number=222222222, names="Partner 1", type=PartnerType.CLIENT, phone_number="2222222222")
|
|
partner2 = Partner(tin_number=333333333, names="Partner 2", type=PartnerType.SUPPLIER, phone_number="3333333333")
|
|
user = User(username="filter_user", password_hash="hashed", role=UserRole.WRITE)
|
|
|
|
integration_session.add_all([partner1, partner2, user])
|
|
integration_session.commit()
|
|
integration_session.refresh(partner1)
|
|
integration_session.refresh(partner2)
|
|
integration_session.refresh(user)
|
|
|
|
# Create transactions for both partners
|
|
assert partner1.id is not None
|
|
assert partner2.id is not None
|
|
assert user.id is not None
|
|
|
|
trans1 = Transaction(
|
|
total_amount=100, transcation_type=TransactionType.SALE, transaction_status=TransactionStatus.PAID,
|
|
partner_id=partner1.id, created_by=user.id, updated_by=user.id
|
|
)
|
|
trans2 = Transaction(
|
|
total_amount=200, transcation_type=TransactionType.PURCHASE, transaction_status=TransactionStatus.UNPAID,
|
|
partner_id=partner2.id, created_by=user.id, updated_by=user.id
|
|
)
|
|
integration_session.add_all([trans1, trans2])
|
|
integration_session.commit()
|
|
|
|
# Filter transactions by partner1
|
|
response = integration_client.get(f"/api/v1/transactions/?partner_id={partner1.id}", headers=integration_admin_token)
|
|
assert response.status_code == 200
|
|
|
|
transactions = response.json()
|
|
assert len(transactions) == 1
|
|
assert transactions[0]["partner_id"] == partner1.id
|
|
|
|
|
|
class TestInventoryAPIIntegration:
|
|
"""Test Inventory API endpoints with database integration."""
|
|
|
|
def test_create_inventory_with_product_relationship(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test creating inventory through API with valid product relationship."""
|
|
# Create product in database
|
|
product = Product(product_code="TST001", product_name="Test Product", purchase_price=90, selling_price=100)
|
|
integration_session.add(product)
|
|
integration_session.commit()
|
|
integration_session.refresh(product)
|
|
|
|
inventory_data = {
|
|
"total_qty": 50,
|
|
"product_id": product.id
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/inventory/", json=inventory_data, headers=integration_admin_token)
|
|
assert response.status_code == 201
|
|
|
|
created_inventory = response.json()
|
|
assert created_inventory["total_qty"] == 50
|
|
assert created_inventory["product_id"] == product.id
|
|
|
|
def test_inventory_unique_product_constraint_through_api(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test inventory unique product constraint enforcement through API."""
|
|
# Create product and inventory directly in database
|
|
product = Product(product_code="UNQ001", product_name="Unique Product", purchase_price=180, selling_price=200)
|
|
integration_session.add(product)
|
|
integration_session.commit()
|
|
integration_session.refresh(product)
|
|
|
|
assert product.id is not None
|
|
inventory = Inventory(
|
|
total_qty=30, product_id=product.id
|
|
)
|
|
integration_session.add(inventory)
|
|
integration_session.commit()
|
|
|
|
# Try to create duplicate inventory for same product through API
|
|
duplicate_data = {
|
|
"total_qty": 20,
|
|
"product_id": product.id
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/inventory/", json=duplicate_data, headers=integration_admin_token)
|
|
assert response.status_code == 400 # Should fail due to unique constraint
|
|
|
|
|
|
class TestCreditAPIIntegration:
|
|
"""Test Credit API endpoints with database integration."""
|
|
|
|
def test_create_credit_with_relationships(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test creating credit through API with valid partner relationship."""
|
|
# Create partner in database
|
|
partner = Partner(tin_number=444444444, names="Credit Partner", type=PartnerType.CLIENT, phone_number="4444444444")
|
|
integration_session.add(partner)
|
|
integration_session.commit()
|
|
integration_session.refresh(partner)
|
|
|
|
credit_data = {
|
|
"amount": 5000.00,
|
|
"due_date": "2024-12-31",
|
|
"interest_rate": 5.5,
|
|
"partner_id": partner.id
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/credit/", json=credit_data, headers=integration_admin_token)
|
|
assert response.status_code == 201
|
|
|
|
created_credit = response.json()
|
|
assert created_credit["amount"] == 5000.00
|
|
assert created_credit["partner_id"] == partner.id
|
|
|
|
|
|
class TestAPITransactionRollback:
|
|
"""Test API transaction rollback behavior on database errors."""
|
|
|
|
def test_api_transaction_rollback_on_error(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test that API transactions are properly rolled back on validation errors."""
|
|
# Create a user first
|
|
user = User(username="rollback_test", password_hash="hashed", role=UserRole.ADMIN)
|
|
integration_session.add(user)
|
|
integration_session.commit()
|
|
|
|
# Try to create duplicate user (should fail)
|
|
duplicate_data = {
|
|
"username": "rollback_test",
|
|
"password": "different_password",
|
|
"role": "WRITE"
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/users/", json=duplicate_data, headers=integration_admin_token)
|
|
assert response.status_code == 400
|
|
|
|
# Verify original user is still intact
|
|
original_user = integration_session.get(User, user.id)
|
|
assert original_user is not None
|
|
assert original_user.role == UserRole.ADMIN
|
|
|
|
def test_complex_operation_rollback(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test rollback behavior for complex operations involving multiple entities."""
|
|
# Create valid partner and user
|
|
partner = Partner(tin_number=555555555, names="Complex Partner", type=PartnerType.CLIENT, phone_number="5555555555")
|
|
user = User(username="complex_user", password_hash="hashed", role=UserRole.WRITE)
|
|
integration_session.add_all([partner, user])
|
|
integration_session.commit()
|
|
integration_session.refresh(partner)
|
|
integration_session.refresh(user)
|
|
|
|
# Try to create transaction with invalid data (should trigger rollback)
|
|
invalid_transaction_data = {
|
|
"amount": -1000.0, # Negative amount should fail validation
|
|
"transaction_type": "INVALID_TYPE",
|
|
"status": "COMPLETED",
|
|
"partner_id": partner.id,
|
|
"user_id": user.id
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/transactions/", json=invalid_transaction_data, headers=integration_admin_token)
|
|
assert response.status_code in [400, 422] # Should fail validation
|
|
|
|
# Verify no partial data was committed
|
|
transactions = integration_session.exec(select(Transaction)).all()
|
|
assert len([t for t in transactions if t.partner_id == partner.id]) == 0
|
|
|
|
|
|
class TestAPIConstraintValidation:
|
|
"""Test database constraint validation through API endpoints."""
|
|
|
|
def test_foreign_key_validation_through_api(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test foreign key constraint validation through API."""
|
|
# Try to create payment with invalid transaction ID
|
|
payment_data = {
|
|
"amount": 100.00,
|
|
"method": "CASH",
|
|
"transaction_id": 99999 # Invalid transaction ID
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/payments/", json=payment_data, headers=integration_admin_token)
|
|
assert response.status_code in [400, 422] # Should fail due to foreign key constraint
|
|
|
|
def test_data_validation_through_api(self, integration_client: TestClient, integration_session: Session, integration_admin_token):
|
|
"""Test data type and format validation through API."""
|
|
# Try to create user with invalid data
|
|
invalid_user_data = {
|
|
"username": "", # Empty username should fail validation
|
|
"password": "short", # Too short password
|
|
"role": "INVALID_ROLE" # Invalid role
|
|
}
|
|
|
|
response = integration_client.post("/api/v1/users/", json=invalid_user_data, headers=integration_admin_token)
|
|
assert response.status_code == 422 # Should fail validation
|