"""Tests for statistics router. T32-T33: Tests for stats endpoints - RED phase """ from datetime import date, timedelta from decimal import Decimal from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient from openrouter_monitor.schemas.stats import ( DashboardResponse, StatsByDate, StatsByModel, StatsSummary, ) class TestDashboardEndpoint: """Tests for GET /api/stats/dashboard endpoint.""" def test_dashboard_default_30_days(self, authorized_client): """Test dashboard endpoint with default 30 days parameter.""" # Arrange with patch("openrouter_monitor.routers.stats.get_dashboard_data") as mock_get_dashboard: mock_get_dashboard.return_value = DashboardResponse( 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=600, cost=Decimal("4.00"), percentage_requests=60.0, percentage_cost=70.4), ], by_date=[ StatsByDate(date=date(2024, 1, 1), requests_count=50, cost=Decimal("0.25")), ], top_models=["gpt-4"], ) # Act response = authorized_client.get("/api/stats/dashboard") # Assert assert response.status_code == 200 data = response.json() assert "summary" in data assert data["summary"]["total_requests"] == 1000 assert data["summary"]["period_days"] == 30 assert "by_model" in data assert "by_date" in data assert "top_models" in data def test_dashboard_custom_days(self, authorized_client): """Test dashboard endpoint with custom days parameter.""" # Arrange with patch("openrouter_monitor.routers.stats.get_dashboard_data") as mock_get_dashboard: mock_get_dashboard.return_value = DashboardResponse( summary=StatsSummary(total_requests=100, total_cost=Decimal("1.00"), period_days=7), by_model=[], by_date=[], top_models=[], ) # Act response = authorized_client.get("/api/stats/dashboard?days=7") # Assert assert response.status_code == 200 data = response.json() assert data["summary"]["period_days"] == 7 def test_dashboard_max_365_days(self, authorized_client): """Test dashboard endpoint enforces max 365 days limit.""" # Act - Request more than 365 days response = authorized_client.get("/api/stats/dashboard?days=400") # Assert - Should get validation error assert response.status_code == 422 data = response.json() assert "detail" in data def test_dashboard_min_1_day(self, authorized_client): """Test dashboard endpoint enforces min 1 day limit.""" # Act - Request less than 1 day response = authorized_client.get("/api/stats/dashboard?days=0") # Assert - Should get validation error assert response.status_code == 422 data = response.json() assert "detail" in data def test_dashboard_without_auth(self, client): """Test dashboard endpoint requires authentication.""" # Act response = client.get("/api/stats/dashboard") # Assert assert response.status_code == 401 data = response.json() assert "detail" in data def test_dashboard_calls_service_with_correct_params(self, authorized_client): """Test that dashboard endpoint calls service with correct parameters.""" # Arrange with patch("openrouter_monitor.routers.stats.get_dashboard_data") as mock_get_dashboard: mock_get_dashboard.return_value = DashboardResponse( summary=StatsSummary(total_requests=0, total_cost=Decimal("0"), period_days=60), by_model=[], by_date=[], top_models=[], ) # Act response = authorized_client.get("/api/stats/dashboard?days=60") # Assert assert response.status_code == 200 # Verify service was called with correct params mock_get_dashboard.assert_called_once() args = mock_get_dashboard.call_args assert args[0][2] == 60 # days parameter class TestUsageEndpoint: """Tests for GET /api/usage endpoint.""" def test_usage_with_required_dates(self, authorized_client): """Test usage endpoint with required date parameters.""" # Arrange with patch("openrouter_monitor.routers.stats.get_usage_stats") as mock_get_usage: from openrouter_monitor.schemas.stats import UsageStatsResponse mock_get_usage.return_value = [ UsageStatsResponse( id=1, 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"), created_at="2024-01-15T12:00:00", ) ] # Act response = authorized_client.get("/api/usage?start_date=2024-01-01&end_date=2024-01-31") # Assert assert response.status_code == 200 data = response.json() assert isinstance(data, list) assert len(data) == 1 assert data[0]["model"] == "gpt-4" def test_usage_missing_required_dates(self, authorized_client): """Test usage endpoint requires start_date and end_date.""" # Act - Missing end_date response = authorized_client.get("/api/usage?start_date=2024-01-01") # Assert assert response.status_code == 422 def test_usage_with_api_key_filter(self, authorized_client): """Test usage endpoint with api_key_id filter.""" # Arrange with patch("openrouter_monitor.routers.stats.get_usage_stats") as mock_get_usage: mock_get_usage.return_value = [] # Act response = authorized_client.get( "/api/usage?start_date=2024-01-01&end_date=2024-01-31&api_key_id=5" ) # Assert assert response.status_code == 200 mock_get_usage.assert_called_once() kwargs = mock_get_usage.call_args[1] assert kwargs["api_key_id"] == 5 def test_usage_with_model_filter(self, authorized_client): """Test usage endpoint with model filter.""" # Arrange with patch("openrouter_monitor.routers.stats.get_usage_stats") as mock_get_usage: mock_get_usage.return_value = [] # Act response = authorized_client.get( "/api/usage?start_date=2024-01-01&end_date=2024-01-31&model=gpt-4" ) # Assert assert response.status_code == 200 mock_get_usage.assert_called_once() kwargs = mock_get_usage.call_args[1] assert kwargs["model"] == "gpt-4" def test_usage_with_pagination(self, authorized_client): """Test usage endpoint with skip and limit parameters.""" # Arrange with patch("openrouter_monitor.routers.stats.get_usage_stats") as mock_get_usage: mock_get_usage.return_value = [] # Act response = authorized_client.get( "/api/usage?start_date=2024-01-01&end_date=2024-01-31&skip=10&limit=50" ) # Assert assert response.status_code == 200 mock_get_usage.assert_called_once() kwargs = mock_get_usage.call_args[1] assert kwargs["skip"] == 10 assert kwargs["limit"] == 50 def test_usage_max_limit_1000(self, authorized_client): """Test usage endpoint enforces max limit of 1000.""" # Act - Request more than 1000 response = authorized_client.get( "/api/usage?start_date=2024-01-01&end_date=2024-01-31&limit=1500" ) # Assert assert response.status_code == 422 def test_usage_combined_filters(self, authorized_client): """Test usage endpoint with all filters combined.""" # Arrange with patch("openrouter_monitor.routers.stats.get_usage_stats") as mock_get_usage: mock_get_usage.return_value = [] # Act response = authorized_client.get( "/api/usage?start_date=2024-01-01&end_date=2024-01-31&api_key_id=5&model=gpt-4&skip=0&limit=100" ) # Assert assert response.status_code == 200 mock_get_usage.assert_called_once() kwargs = mock_get_usage.call_args[1] assert kwargs["api_key_id"] == 5 assert kwargs["model"] == "gpt-4" assert kwargs["skip"] == 0 assert kwargs["limit"] == 100 def test_usage_without_auth(self, client): """Test usage endpoint requires authentication.""" # Act response = client.get("/api/usage?start_date=2024-01-01&end_date=2024-01-31") # Assert assert response.status_code == 401 class TestSecurity: """Security tests for stats endpoints.""" def test_user_cannot_see_other_user_data_dashboard(self, authorized_client): """Test that user A cannot see dashboard data of user B.""" # This is tested implicitly by checking that the service is called # with the current user's ID, not by allowing user_id parameter pass def test_user_cannot_see_other_user_data_usage(self, authorized_client): """Test that user A cannot see usage data of user B.""" # This is tested implicitly by the service filtering by user_id pass