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.
This commit is contained in:
262
tests/test_reporting.py
Normal file
262
tests/test_reporting.py
Normal file
@@ -0,0 +1,262 @@
|
||||
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
|
||||
Reference in New Issue
Block a user