"""Tests for statistics Pydantic schemas. T30: Tests for stats schemas - RED phase (test fails before implementation) """ from datetime import date, datetime from decimal import Decimal import pytest from pydantic import ValidationError from openrouter_monitor.schemas.stats import ( DashboardResponse, StatsByDate, StatsByModel, StatsSummary, UsageStatsCreate, UsageStatsResponse, ) class TestUsageStatsCreate: """Tests for UsageStatsCreate schema.""" def test_create_with_valid_data(self): """Test creating UsageStatsCreate with valid data.""" data = { "api_key_id": 1, "date": date(2024, 1, 15), "model": "gpt-4", "requests_count": 100, "tokens_input": 5000, "tokens_output": 3000, "cost": Decimal("0.123456"), } result = UsageStatsCreate(**data) assert result.api_key_id == 1 assert result.date == date(2024, 1, 15) assert result.model == "gpt-4" assert result.requests_count == 100 assert result.tokens_input == 5000 assert result.tokens_output == 3000 assert result.cost == Decimal("0.123456") def test_create_with_minimal_data(self): """Test creating UsageStatsCreate with minimal required data.""" data = { "api_key_id": 1, "date": date(2024, 1, 15), "model": "gpt-3.5-turbo", } result = UsageStatsCreate(**data) assert result.api_key_id == 1 assert result.date == date(2024, 1, 15) assert result.model == "gpt-3.5-turbo" assert result.requests_count == 0 # default assert result.tokens_input == 0 # default assert result.tokens_output == 0 # default assert result.cost == Decimal("0") # default def test_create_with_string_date(self): """Test creating UsageStatsCreate with date as string.""" data = { "api_key_id": 1, "date": "2024-01-15", "model": "claude-3", } result = UsageStatsCreate(**data) assert result.date == date(2024, 1, 15) def test_create_missing_required_fields(self): """Test that missing required fields raise ValidationError.""" with pytest.raises(ValidationError) as exc_info: UsageStatsCreate() errors = exc_info.value.errors() # Pydantic v2 uses 'loc' (location) instead of 'field' assert any("api_key_id" in e["loc"] for e in errors) assert any("date" in e["loc"] for e in errors) assert any("model" in e["loc"] for e in errors) def test_create_empty_model_raises_error(self): """Test that empty model raises ValidationError.""" with pytest.raises(ValidationError) as exc_info: UsageStatsCreate( api_key_id=1, date=date(2024, 1, 15), model="", ) assert "model" in str(exc_info.value) class TestUsageStatsResponse: """Tests for UsageStatsResponse schema with orm_mode.""" def test_response_with_all_fields(self): """Test UsageStatsResponse with all fields.""" data = { "id": 1, "api_key_id": 2, "date": date(2024, 1, 15), "model": "gpt-4", "requests_count": 100, "tokens_input": 5000, "tokens_output": 3000, "cost": Decimal("0.123456"), "created_at": datetime(2024, 1, 15, 12, 0, 0), } result = UsageStatsResponse(**data) assert result.id == 1 assert result.api_key_id == 2 assert result.model == "gpt-4" assert result.cost == Decimal("0.123456") def test_response_from_attributes(self): """Test UsageStatsResponse with from_attributes=True (orm_mode).""" # Simulate SQLAlchemy model object class MockUsageStats: id = 1 api_key_id = 2 date = date(2024, 1, 15) model = "gpt-4" requests_count = 100 tokens_input = 5000 tokens_output = 3000 cost = Decimal("0.123456") created_at = datetime(2024, 1, 15, 12, 0, 0) result = UsageStatsResponse.model_validate(MockUsageStats()) assert result.id == 1 assert result.model == "gpt-4" class TestStatsSummary: """Tests for StatsSummary schema.""" def test_summary_with_all_fields(self): """Test StatsSummary with all aggregation fields.""" data = { "total_requests": 1000, "total_cost": Decimal("5.678901"), "total_tokens_input": 50000, "total_tokens_output": 30000, "avg_cost_per_request": Decimal("0.005679"), "period_days": 30, } result = StatsSummary(**data) assert result.total_requests == 1000 assert result.total_cost == Decimal("5.678901") assert result.total_tokens_input == 50000 assert result.total_tokens_output == 30000 assert result.avg_cost_per_request == Decimal("0.005679") assert result.period_days == 30 def test_summary_defaults(self): """Test StatsSummary default values.""" data = { "total_requests": 100, "total_cost": Decimal("1.00"), } result = StatsSummary(**data) assert result.total_tokens_input == 0 assert result.total_tokens_output == 0 assert result.avg_cost_per_request == Decimal("0") assert result.period_days == 0 class TestStatsByModel: """Tests for StatsByModel schema.""" def test_stats_by_model_with_all_fields(self): """Test StatsByModel with all fields.""" data = { "model": "gpt-4", "requests_count": 500, "cost": Decimal("3.456789"), "percentage_requests": 50.0, "percentage_cost": 60.5, } result = StatsByModel(**data) assert result.model == "gpt-4" assert result.requests_count == 500 assert result.cost == Decimal("3.456789") assert result.percentage_requests == 50.0 assert result.percentage_cost == 60.5 def test_stats_by_model_defaults(self): """Test StatsByModel default values for percentages.""" data = { "model": "gpt-3.5-turbo", "requests_count": 200, "cost": Decimal("0.50"), } result = StatsByModel(**data) assert result.percentage_requests == 0.0 assert result.percentage_cost == 0.0 class TestStatsByDate: """Tests for StatsByDate schema.""" def test_stats_by_date_with_all_fields(self): """Test StatsByDate with all fields.""" data = { "date": date(2024, 1, 15), "requests_count": 100, "cost": Decimal("0.567890"), } result = StatsByDate(**data) assert result.date == date(2024, 1, 15) assert result.requests_count == 100 assert result.cost == Decimal("0.567890") def test_stats_by_date_with_string_date(self): """Test StatsByDate with date as string.""" data = { "date": "2024-12-25", "requests_count": 50, "cost": Decimal("0.25"), } result = StatsByDate(**data) assert result.date == date(2024, 12, 25) class TestDashboardResponse: """Tests for DashboardResponse schema.""" def test_dashboard_response_complete(self): """Test DashboardResponse with complete data.""" summary = StatsSummary( total_requests=1000, total_cost=Decimal("5.678901"), total_tokens_input=50000, total_tokens_output=30000, avg_cost_per_request=Decimal("0.005679"), period_days=30, ) by_model = [ StatsByModel( model="gpt-4", requests_count=500, cost=Decimal("3.456789"), percentage_requests=50.0, percentage_cost=60.5, ), StatsByModel( model="gpt-3.5-turbo", requests_count=500, cost=Decimal("2.222112"), percentage_requests=50.0, percentage_cost=39.5, ), ] by_date = [ StatsByDate(date=date(2024, 1, 1), requests_count=50, cost=Decimal("0.25")), StatsByDate(date=date(2024, 1, 2), requests_count=75, cost=Decimal("0.375")), ] top_models = ["gpt-4", "gpt-3.5-turbo"] result = DashboardResponse( summary=summary, by_model=by_model, by_date=by_date, top_models=top_models, ) assert result.summary.total_requests == 1000 assert len(result.by_model) == 2 assert len(result.by_date) == 2 assert result.top_models == ["gpt-4", "gpt-3.5-turbo"] def test_dashboard_response_empty_lists(self): """Test DashboardResponse with empty lists.""" summary = StatsSummary( total_requests=0, total_cost=Decimal("0"), ) result = DashboardResponse( summary=summary, by_model=[], by_date=[], top_models=[], ) assert result.by_model == [] assert result.by_date == [] assert result.top_models == [] def test_dashboard_response_missing_top_models(self): """Test DashboardResponse without top_models (optional).""" summary = StatsSummary(total_requests=100, total_cost=Decimal("1.00")) result = DashboardResponse( summary=summary, by_model=[], by_date=[], ) assert result.top_models == []