- 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.
263 lines
10 KiB
Python
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
|