from __future__ import annotations from fastapi.testclient import TestClient from unittest.mock import Mock from models import Project, Scenario from services.reporting import ( ReportingService, ReportFilters, IncludeOptions, ScenarioReport, ScenarioFinancialTotals, ScenarioDeterministicMetrics, ) from routes.reports import router as reports_router class TestReportingService: def test_build_project_summary_context(self, unit_of_work_factory): with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario = Scenario(project_id=project.id, name="Test Scenario", status="draft") uow.scenarios.create(scenario) uow.commit() service = ReportingService(uow) request = Mock() request.url_for = Mock(return_value="/api/reports/projects/1") filters = ReportFilters() include = IncludeOptions() context = service.build_project_summary_context( project, filters, include, 500, (5.0, 50.0, 95.0), request ) assert "project" in context assert context["scenario_count"] == 1 assert "aggregates" in context assert "scenarios" in context assert context["title"] == f"Project Summary · {project.name}" def test_build_scenario_comparison_context(self, unit_of_work_factory): with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario1 = Scenario(project_id=project.id, name="Scenario 1", status="draft") scenario2 = Scenario(project_id=project.id, name="Scenario 2", status="active") uow.scenarios.create(scenario1) uow.scenarios.create(scenario2) uow.commit() service = ReportingService(uow) request = Mock() request.url_for = Mock( return_value="/api/reports/projects/1/comparison") include = IncludeOptions() scenarios = [scenario1, scenario2] context = service.build_scenario_comparison_context( project, scenarios, include, 500, (5.0, 50.0, 95.0), request ) assert "project" in context assert "scenarios" in context assert "comparison" in context assert context["title"] == f"Scenario Comparison · {project.name}" def test_build_scenario_distribution_context(self, unit_of_work_factory): with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario = Scenario(project_id=project.id, name="Test Scenario", status="draft") uow.scenarios.create(scenario) uow.commit() service = ReportingService(uow) request = Mock() request.url_for = Mock( return_value="/api/reports/scenarios/1/distribution") include = IncludeOptions() context = service.build_scenario_distribution_context( scenario, include, 500, (5.0, 50.0, 95.0), request ) assert "scenario" in context assert "summary" in context assert "metrics" in context assert "monte_carlo" in context assert context["title"] == f"Scenario Distribution · {scenario.name}" def test_scenario_report_to_dict_with_enum_status(self, unit_of_work_factory): """Test that to_dict handles enum status values correctly.""" with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario = Scenario( project_id=project.id, name="Test Scenario", status="draft", # Stored as string primary_resource="diesel" # Stored as string ) uow.scenarios.create(scenario) uow.commit() # Create a mock scenario report totals = ScenarioFinancialTotals( currency="USD", inflows=1000.0, outflows=500.0, net=500.0, by_category={} ) deterministic = ScenarioDeterministicMetrics( currency="USD", discount_rate=0.1, compounds_per_year=1, npv=100.0, irr=0.15, payback_period=2.5, notes=[] ) report = ScenarioReport( scenario=scenario, totals=totals, deterministic=deterministic, monte_carlo=None ) result = report.to_dict() assert result["scenario"]["status"] == "draft" # type: ignore # type: ignore assert result["scenario"]["primary_resource"] == "diesel" assert result["financials"]["net"] == 500.0 # type: ignore assert result["metrics"]["npv"] == 100.0 # type: ignore def test_project_summary_with_scenario_ids_filter(self, unit_of_work_factory): with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario1 = Scenario(project_id=project.id, name="Scenario 1", status="active") scenario2 = Scenario(project_id=project.id, name="Scenario 2", status="draft") uow.scenarios.create(scenario1) uow.scenarios.create(scenario2) uow.commit() service = ReportingService(uow) # Test filtering by scenario IDs filters = ReportFilters(scenario_ids={scenario1.id}) result = service.project_summary( project, filters=filters, include=IncludeOptions(), iterations=100, percentiles=(5.0, 50.0, 95.0) ) assert result["scenario_count"] == 1 # type: ignore # type: ignore # type: ignore assert result["scenarios"][0]["scenario"]["name"] == "Scenario 1" class TestReportingRoutes: def test_project_summary_route(self, client: TestClient, unit_of_work_factory): with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario = Scenario(project_id=project.id, name="Test Scenario", status="draft") uow.scenarios.create(scenario) uow.commit() response = client.get(f"/reports/projects/{project.id}") assert response.status_code == 200 data = response.json() assert "project" in data assert data["scenario_count"] == 1 def test_project_summary_html_route(self, client: TestClient, unit_of_work_factory): with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario = Scenario(project_id=project.id, name="Test Scenario", status="draft") uow.scenarios.create(scenario) uow.commit() response = client.get(f"/reports/projects/{project.id}/ui") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] assert "Test Project" in response.text def test_scenario_comparison_route(self, client: TestClient, unit_of_work_factory): with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario1 = Scenario(project_id=project.id, name="Scenario 1", status="draft") scenario2 = Scenario(project_id=project.id, name="Scenario 2", status="active") uow.scenarios.create(scenario1) uow.scenarios.create(scenario2) uow.commit() response = client.get( f"/reports/projects/{project.id}/scenarios/compare?scenario_ids={scenario1.id}&scenario_ids={scenario2.id}" ) assert response.status_code == 200 data = response.json() assert "project" in data assert "scenarios" in data assert "comparison" in data def test_scenario_distribution_route(self, client: TestClient, unit_of_work_factory): with unit_of_work_factory() as uow: project = Project(name="Test Project", location="Test Location") uow.projects.create(project) scenario = Scenario(project_id=project.id, name="Test Scenario", status="draft") uow.scenarios.create(scenario) uow.commit() response = client.get( f"/reports/scenarios/{scenario.id}/distribution") assert response.status_code == 200 data = response.json() assert "scenario" in data assert "summary" in data assert "monte_carlo" in data def test_unauthorized_access(self, client: TestClient): # Create a new client without authentication from fastapi import FastAPI app = FastAPI() app.include_router(reports_router) from fastapi.testclient import TestClient unauth_client = TestClient(app) response = unauth_client.get("/reports/projects/1") assert response.status_code == 401 def test_project_not_found(self, client: TestClient): response = client.get("/reports/projects/999") assert response.status_code == 404 def test_scenario_not_found(self, client: TestClient): response = client.get("/reports/scenarios/999/distribution") assert response.status_code == 404