Add statistics router with two endpoints: - GET /api/stats/dashboard: Aggregated dashboard statistics - Query param: days (1-365, default 30) - Auth required - Returns DashboardResponse - GET /api/usage: Detailed usage statistics with filtering - Required params: start_date, end_date - Optional filters: api_key_id, model - Pagination: skip, limit (max 1000) - Auth required - Returns List[UsageStatsResponse] Also add get_usage_stats() service function for querying individual usage records with filtering and pagination.
273 lines
9.8 KiB
Python
273 lines
9.8 KiB
Python
"""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
|