Files
calminer/tests/test_reporting.py
zwitschi ce9c174b53
Some checks failed
CI / lint (push) Failing after 1m14s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
feat: Enhance project and scenario creation with monitoring metrics
- Added monitoring metrics for project creation success and error handling in `ProjectRepository`.
- Implemented similar monitoring for scenario creation in `ScenarioRepository`.
- Refactored `run_monte_carlo` function in `simulation.py` to include timing and success/error metrics.
- Introduced new CSS styles for headers, alerts, and navigation buttons in `main.css` and `projects.css`.
- Created a new JavaScript file for navigation logic to handle chevron buttons.
- Updated HTML templates to include new navigation buttons and improved styling for buttons.
- Added tests for reporting service and routes to ensure proper functionality and access control.
- Removed unused imports and optimized existing test files for better clarity and performance.
2025-11-12 10:36:24 +01:00

263 lines
10 KiB
Python

from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from unittest.mock import Mock
from models import Project, Scenario, FinancialInput
from models.metadata import CostBucket, ResourceType
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
from routes.reports import router as reports_router
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