import pytest import asyncio from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker import json import time import random import string from app.main import app from app.core.database import get_db, Base from app.models.trip import Trip from app.models.participant import Participant from app.models.expense import Expense, ExpenseSplit # Test database setup SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def override_get_db(): try: db = TestingSessionLocal() yield db finally: db.close() app.dependency_overrides[get_db] = override_get_db # Create test client client = TestClient(app) def setup_database(): """Setup test database""" Base.metadata.create_all(bind=engine) def cleanup_database(): """Clean up test database""" Base.metadata.drop_all(bind=engine) def generate_share_code(): """Generate a random 6-character share code""" return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) def create_test_trip(): """Helper function to create a test trip""" trip_data = { "name": "Test Trip Paris", "creator_name": "John Doe", "currency_code": "USD" } response = client.post("/api/trips/", json=trip_data) return response.json() def create_test_participant(trip_id, name="Test Participant"): """Helper function to create a test participant""" participant_data = {"name": name} response = client.post(f"/api/participants/{trip_id}/participants", json=participant_data) return response.json() class TestExpenseAPI: """Test suite for Expense API endpoints""" @pytest.fixture(autouse=True) def setup(self): """Setup for each test""" setup_database() yield cleanup_database() def test_create_expense(self): """Test creating a new expense""" # Create trip and participants trip = create_test_trip() participant1 = create_test_participant(trip["id"], "Alice") participant2 = create_test_participant(trip["id"], "Bob") # Create expense data expense_data = { "description": "Dinner at Restaurant", "amount": 100.50, "currency_code": "USD", "exchange_rate": 1.0, "category": "Food & Dining", "paid_by_id": participant1["id"], "splits": [ {"participant_id": participant1["id"], "percentage": 50.0}, {"participant_id": participant2["id"], "percentage": 50.0} ] } # Create expense response = client.post(f"/api/expenses/{trip['id']}/expenses", json=expense_data) assert response.status_code == 200 expense = response.json() assert expense["description"] == "Dinner at Restaurant" assert expense["amount"] == 100.50 assert expense["currency_code"] == "USD" assert expense["category"] == "Food & Dining" assert expense["paid_by_id"] == participant1["id"] assert len(expense["splits"]) == 2 assert expense["amount_in_trip_currency"] == 100.50 # Check splits splits = expense["splits"] alice_split = next(s for s in splits if s["participant_id"] == participant1["id"]) bob_split = next(s for s in splits if s["participant_id"] == participant2["id"]) assert alice_split["percentage"] == 50.0 assert alice_split["amount"] == 50.25 assert bob_split["percentage"] == 50.0 assert bob_split["amount"] == 50.25 def test_create_expense_with_currency_conversion(self): """Test creating an expense with currency conversion""" # Create trip with EUR currency trip_data = { "name": "Europe Trip", "creator_name": "Alice", "currency_code": "EUR" } trip_response = client.post("/api/trips/", json=trip_data) trip = trip_response.json() participant1 = create_test_participant(trip["id"], "Alice") participant2 = create_test_participant(trip["id"], "Bob") # Create expense in USD expense_data = { "description": "Hotel Booking", "amount": 120.00, "currency_code": "USD", "exchange_rate": 0.85, # 1 USD = 0.85 EUR "category": "Accommodation", "paid_by_id": participant1["id"], "splits": [ {"participant_id": participant1["id"], "percentage": 100.0} ] } response = client.post(f"/api/expenses/{trip['id']}/expenses", json=expense_data) assert response.status_code == 200 expense = response.json() assert expense["amount"] == 120.00 assert expense["currency_code"] == "USD" assert expense["amount_in_trip_currency"] == 102.00 # 120 * 0.85 def test_get_expenses(self): """Test retrieving expenses for a trip""" # Create trip and participants trip = create_test_trip() participant1 = create_test_participant(trip["id"], "Alice") participant2 = create_test_participant(trip["id"], "Bob") # Create multiple expenses expenses = [ { "description": "Lunch", "amount": 25.00, "currency_code": "USD", "exchange_rate": 1.0, "category": "Food & Dining", "paid_by_id": participant1["id"], "splits": [ {"participant_id": participant1["id"], "percentage": 50.0}, {"participant_id": participant2["id"], "percentage": 50.0} ] }, { "description": "Taxi", "amount": 15.00, "currency_code": "USD", "exchange_rate": 1.0, "category": "Transportation", "paid_by_id": participant2["id"], "splits": [ {"participant_id": participant1["id"], "percentage": 100.0} ] } ] for expense_data in expenses: client.post(f"/api/expenses/{trip['id']}/expenses", json=expense_data) # Get expenses response = client.get(f"/api/expenses/{trip['id']}/expenses") assert response.status_code == 200 retrieved_expenses = response.json() assert len(retrieved_expenses) == 2 # Check expense details expense_descriptions = [e["description"] for e in retrieved_expenses] assert "Lunch" in expense_descriptions assert "Taxi" in expense_descriptions def test_expense_validation(self): """Test expense validation""" trip = create_test_trip() participant = create_test_participant(trip["id"], "Alice") # Test invalid expense (splits don't total 100%) invalid_expense = { "description": "Invalid Expense", "amount": 100.00, "currency_code": "USD", "exchange_rate": 1.0, "category": "Other", "paid_by_id": participant["id"], "splits": [ {"participant_id": participant["id"], "percentage": 80.0} # Only 80% ] } response = client.post(f"/api/expenses/{trip['id']}/expenses", json=invalid_expense) assert response.status_code == 400 assert "Splits must total 100%" in response.json()["detail"] # Test negative amount invalid_expense = { "description": "Negative Amount", "amount": -50.00, "currency_code": "USD", "exchange_rate": 1.0, "category": "Other", "paid_by_id": participant["id"], "splits": [ {"participant_id": participant["id"], "percentage": 100.0} ] } response = client.post(f"/api/expenses/{trip['id']}/expenses", json=invalid_expense) assert response.status_code == 422 def test_expense_categories(self): """Test expense with different categories""" trip = create_test_trip() participant = create_test_participant(trip["id"], "Alice") categories = [ "Food & Dining", "Transportation", "Accommodation", "Entertainment", "Shopping", "Healthcare", "Education", "Business", "Other" ] for category in categories: expense_data = { "description": f"Test {category} expense", "amount": 50.00, "currency_code": "USD", "exchange_rate": 1.0, "category": category, "paid_by_id": participant["id"], "splits": [ {"participant_id": participant["id"], "percentage": 100.0} ] } response = client.post(f"/api/expenses/{trip['id']}/expenses", json=expense_data) assert response.status_code == 200 expense = response.json() assert expense["category"] == category def test_get_trip_summary(self): """Test retrieving trip summary with expenses""" # Create trip and participants trip = create_test_trip() participant1 = create_test_participant(trip["id"], "Alice") participant2 = create_test_participant(trip["id"], "Bob") # Create expenses expenses = [ { "description": "Restaurant (Alice paid, split equally)", "amount": 100.00, "currency_code": "USD", "exchange_rate": 1.0, "category": "Food & Dining", "paid_by_id": participant1["id"], "splits": [ {"participant_id": participant1["id"], "percentage": 50.0}, {"participant_id": participant2["id"], "percentage": 50.0} ] }, { "description": "Taxi (Bob paid, Alice uses)", "amount": 40.00, "currency_code": "USD", "exchange_rate": 1.0, "category": "Transportation", "paid_by_id": participant2["id"], "splits": [ {"participant_id": participant1["id"], "percentage": 100.0} ] } ] for expense_data in expenses: client.post(f"/api/expenses/{trip['id']}/expenses", json=expense_data) # Get trip summary response = client.get(f"/api/trips/{trip['id']}") assert response.status_code == 200 trip_data = response.json() assert trip_data["total_expenses"] == 140.00 assert trip_data["currency_code"] == "USD" if __name__ == "__main__": pytest.main([__file__, "-v"])