feat: add scenarios list page with metrics and quick actions
- Introduced a new template for listing scenarios associated with a project. - Added metrics for total, active, draft, and archived scenarios. - Implemented quick actions for creating new scenarios and reviewing project overview. - Enhanced navigation with breadcrumbs for better user experience. refactor: update Opex and Profitability templates for consistency - Changed titles and button labels for clarity in Opex and Profitability templates. - Updated form IDs and action URLs for better alignment with new naming conventions. - Improved navigation links to include scenario and project overviews. test: add integration tests for Opex calculations - Created new tests for Opex calculation HTML and JSON flows. - Validated successful calculations and ensured correct data persistence. - Implemented tests for currency mismatch and unsupported frequency scenarios. test: enhance project and scenario route tests - Added tests to verify scenario list rendering and calculator shortcuts. - Ensured scenario detail pages link back to the portfolio correctly. - Validated project detail pages show associated scenarios accurately.
This commit is contained in:
@@ -48,7 +48,7 @@ def test_capex_calculation_html_flow(
|
||||
f"/calculations/capex?project_id={project_id}&scenario_id={scenario_id}"
|
||||
)
|
||||
assert form_page.status_code == 200
|
||||
assert "Initial Capex Planner" in form_page.text
|
||||
assert "Capex Planner" in form_page.text
|
||||
|
||||
response = client.post(
|
||||
f"/calculations/capex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
@@ -71,7 +71,7 @@ def test_capex_calculation_html_flow(
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "Initial capex calculation completed successfully." in response.text
|
||||
assert "Capex calculation completed successfully." in response.text
|
||||
assert "Capex Summary" in response.text
|
||||
assert "$1,200,000.00" in response.text or "1,200,000" in response.text
|
||||
assert "USD" in response.text
|
||||
|
||||
@@ -15,7 +15,7 @@ def _create_project(client: TestClient, name: str) -> int:
|
||||
"name": name,
|
||||
"location": "Nevada",
|
||||
"operation_type": "open_pit",
|
||||
"description": "Project for processing opex testing",
|
||||
"description": "Project for opex testing",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
@@ -37,7 +37,7 @@ def _create_scenario(client: TestClient, project_id: int, name: str) -> int:
|
||||
return response.json()["id"]
|
||||
|
||||
|
||||
def test_processing_opex_calculation_html_flow(
|
||||
def test_opex_calculation_html_flow(
|
||||
client: TestClient,
|
||||
unit_of_work_factory: Callable[[], UnitOfWork],
|
||||
) -> None:
|
||||
@@ -45,13 +45,13 @@ def test_processing_opex_calculation_html_flow(
|
||||
scenario_id = _create_scenario(client, project_id, "Opex HTML Scenario")
|
||||
|
||||
form_page = client.get(
|
||||
f"/calculations/processing-opex?project_id={project_id}&scenario_id={scenario_id}"
|
||||
f"/calculations/opex?project_id={project_id}&scenario_id={scenario_id}"
|
||||
)
|
||||
assert form_page.status_code == 200
|
||||
assert "Processing Opex Planner" in form_page.text
|
||||
assert "Opex Planner" in form_page.text
|
||||
|
||||
response = client.post(
|
||||
f"/calculations/processing-opex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
f"/calculations/opex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
data={
|
||||
"components[0][name]": "Power",
|
||||
"components[0][category]": "energy",
|
||||
@@ -75,21 +75,21 @@ def test_processing_opex_calculation_html_flow(
|
||||
"parameters[evaluation_horizon_years]": "3",
|
||||
"parameters[apply_escalation]": "1",
|
||||
"options[persist]": "1",
|
||||
"options[snapshot_notes]": "Processing opex HTML flow",
|
||||
"options[snapshot_notes]": "Opex HTML flow",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "Processing opex calculation completed successfully." in response.text
|
||||
assert "Opex calculation completed successfully." in response.text
|
||||
assert "Opex Summary" in response.text
|
||||
assert "$22,000.00" in response.text or "22,000" in response.text
|
||||
|
||||
with unit_of_work_factory() as uow:
|
||||
assert uow.project_processing_opex is not None
|
||||
assert uow.scenario_processing_opex is not None
|
||||
assert uow.project_opex is not None
|
||||
assert uow.scenario_opex is not None
|
||||
|
||||
project_snapshots = uow.project_processing_opex.list_for_project(
|
||||
project_snapshots = uow.project_opex.list_for_project(
|
||||
project_id)
|
||||
scenario_snapshots = uow.scenario_processing_opex.list_for_scenario(
|
||||
scenario_snapshots = uow.scenario_opex.list_for_scenario(
|
||||
scenario_id)
|
||||
|
||||
assert len(project_snapshots) == 1
|
||||
@@ -119,7 +119,7 @@ def test_processing_opex_calculation_html_flow(
|
||||
assert scenario_snapshot.currency_code == "USD"
|
||||
|
||||
|
||||
def test_processing_opex_calculation_json_flow(
|
||||
def test_opex_calculation_json_flow(
|
||||
client: TestClient,
|
||||
unit_of_work_factory: Callable[[], UnitOfWork],
|
||||
) -> None:
|
||||
@@ -170,7 +170,7 @@ def test_processing_opex_calculation_json_flow(
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
f"/calculations/processing-opex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
f"/calculations/opex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
json=payload,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -206,12 +206,12 @@ def test_processing_opex_calculation_json_flow(
|
||||
assert data["metrics"]["annual_average"] == pytest.approx(expected_average)
|
||||
|
||||
with unit_of_work_factory() as uow:
|
||||
assert uow.project_processing_opex is not None
|
||||
assert uow.scenario_processing_opex is not None
|
||||
assert uow.project_opex is not None
|
||||
assert uow.scenario_opex is not None
|
||||
|
||||
project_snapshot = uow.project_processing_opex.latest_for_project(
|
||||
project_snapshot = uow.project_opex.latest_for_project(
|
||||
project_id)
|
||||
scenario_snapshot = uow.scenario_processing_opex.latest_for_scenario(
|
||||
scenario_snapshot = uow.scenario_opex.latest_for_scenario(
|
||||
scenario_id)
|
||||
|
||||
assert project_snapshot is not None
|
||||
@@ -232,7 +232,7 @@ def test_processing_opex_calculation_json_flow(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("content_type", ["form", "json"])
|
||||
def test_processing_opex_calculation_currency_mismatch(
|
||||
def test_opex_calculation_currency_mismatch(
|
||||
client: TestClient,
|
||||
unit_of_work_factory: Callable[[], UnitOfWork],
|
||||
content_type: str,
|
||||
@@ -260,7 +260,7 @@ def test_processing_opex_calculation_currency_mismatch(
|
||||
"options": {"persist": True},
|
||||
}
|
||||
response = client.post(
|
||||
f"/calculations/processing-opex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
f"/calculations/opex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
json=payload,
|
||||
)
|
||||
assert response.status_code == 422
|
||||
@@ -270,7 +270,7 @@ def test_processing_opex_calculation_currency_mismatch(
|
||||
"components[0].currency" in entry for entry in body.get("errors", []))
|
||||
else:
|
||||
response = client.post(
|
||||
f"/calculations/processing-opex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
f"/calculations/opex?project_id={project_id}&scenario_id={scenario_id}",
|
||||
data={
|
||||
"components[0][name]": "Power",
|
||||
"components[0][category]": "energy",
|
||||
@@ -298,12 +298,12 @@ def test_processing_opex_calculation_currency_mismatch(
|
||||
"components[0].currency" in entry for entry in combined_errors)
|
||||
|
||||
with unit_of_work_factory() as uow:
|
||||
assert uow.project_processing_opex is not None
|
||||
assert uow.scenario_processing_opex is not None
|
||||
assert uow.project_opex is not None
|
||||
assert uow.scenario_opex is not None
|
||||
|
||||
project_snapshots = uow.project_processing_opex.list_for_project(
|
||||
project_snapshots = uow.project_opex.list_for_project(
|
||||
project_id)
|
||||
scenario_snapshots = uow.scenario_processing_opex.list_for_scenario(
|
||||
scenario_snapshots = uow.scenario_opex.list_for_scenario(
|
||||
scenario_id)
|
||||
|
||||
assert project_snapshots == []
|
||||
@@ -1,16 +1,16 @@
|
||||
import pytest
|
||||
|
||||
from schemas.calculations import (
|
||||
ProcessingOpexCalculationRequest,
|
||||
ProcessingOpexComponentInput,
|
||||
ProcessingOpexOptions,
|
||||
ProcessingOpexParameters,
|
||||
OpexCalculationRequest,
|
||||
OpexComponentInput,
|
||||
OpexOptions,
|
||||
OpexParameters,
|
||||
)
|
||||
from services.calculations import calculate_processing_opex
|
||||
from services.calculations import calculate_opex
|
||||
from services.exceptions import OpexValidationError
|
||||
|
||||
|
||||
def _component(**overrides) -> ProcessingOpexComponentInput:
|
||||
def _component(**overrides) -> OpexComponentInput:
|
||||
defaults = {
|
||||
"id": None,
|
||||
"name": "Component",
|
||||
@@ -24,11 +24,11 @@ def _component(**overrides) -> ProcessingOpexComponentInput:
|
||||
"notes": None,
|
||||
}
|
||||
defaults.update(overrides)
|
||||
return ProcessingOpexComponentInput(**defaults)
|
||||
return OpexComponentInput(**defaults)
|
||||
|
||||
|
||||
def test_calculate_processing_opex_success():
|
||||
request = ProcessingOpexCalculationRequest(
|
||||
def test_calculate_opex_success():
|
||||
request = OpexCalculationRequest(
|
||||
components=[
|
||||
_component(
|
||||
name="Power",
|
||||
@@ -49,17 +49,17 @@ def test_calculate_processing_opex_success():
|
||||
period_end=2,
|
||||
),
|
||||
],
|
||||
parameters=ProcessingOpexParameters(
|
||||
parameters=OpexParameters(
|
||||
currency_code="USD",
|
||||
escalation_pct=5,
|
||||
discount_rate_pct=None,
|
||||
evaluation_horizon_years=2,
|
||||
apply_escalation=True,
|
||||
),
|
||||
options=ProcessingOpexOptions(persist=True, snapshot_notes=None),
|
||||
options=OpexOptions(persist=True, snapshot_notes=None),
|
||||
)
|
||||
|
||||
result = calculate_processing_opex(request)
|
||||
result = calculate_opex(request)
|
||||
|
||||
assert result.currency == "USD"
|
||||
assert result.options.persist is True
|
||||
@@ -89,10 +89,10 @@ def test_calculate_processing_opex_success():
|
||||
assert result.components[1].frequency == "quarterly"
|
||||
|
||||
|
||||
def test_calculate_processing_opex_currency_mismatch():
|
||||
request = ProcessingOpexCalculationRequest(
|
||||
def test_calculate_opex_currency_mismatch():
|
||||
request = OpexCalculationRequest(
|
||||
components=[_component(currency="USD")],
|
||||
parameters=ProcessingOpexParameters(
|
||||
parameters=OpexParameters(
|
||||
currency_code="CAD",
|
||||
escalation_pct=None,
|
||||
discount_rate_pct=None,
|
||||
@@ -101,16 +101,16 @@ def test_calculate_processing_opex_currency_mismatch():
|
||||
)
|
||||
|
||||
with pytest.raises(OpexValidationError) as exc:
|
||||
calculate_processing_opex(request)
|
||||
calculate_opex(request)
|
||||
|
||||
assert "Component currency does not match" in exc.value.message
|
||||
assert exc.value.field_errors and "components[0].currency" in exc.value.field_errors[0]
|
||||
|
||||
|
||||
def test_calculate_processing_opex_unsupported_frequency():
|
||||
request = ProcessingOpexCalculationRequest(
|
||||
def test_calculate_opex_unsupported_frequency():
|
||||
request = OpexCalculationRequest(
|
||||
components=[_component(frequency="biweekly")],
|
||||
parameters=ProcessingOpexParameters(
|
||||
parameters=OpexParameters(
|
||||
currency_code="USD",
|
||||
escalation_pct=None,
|
||||
discount_rate_pct=None,
|
||||
@@ -119,28 +119,28 @@ def test_calculate_processing_opex_unsupported_frequency():
|
||||
)
|
||||
|
||||
with pytest.raises(OpexValidationError) as exc:
|
||||
calculate_processing_opex(request)
|
||||
calculate_opex(request)
|
||||
|
||||
assert "Unsupported frequency" in exc.value.message
|
||||
assert exc.value.field_errors and "components[0].frequency" in exc.value.field_errors[0]
|
||||
|
||||
|
||||
def test_calculate_processing_opex_requires_components():
|
||||
request = ProcessingOpexCalculationRequest(components=[])
|
||||
def test_calculate_opex_requires_components():
|
||||
request = OpexCalculationRequest(components=[])
|
||||
|
||||
with pytest.raises(OpexValidationError) as exc:
|
||||
calculate_processing_opex(request)
|
||||
calculate_opex(request)
|
||||
|
||||
assert "At least one processing opex component" in exc.value.message
|
||||
assert "At least one opex component" in exc.value.message
|
||||
assert exc.value.field_errors and "components" in exc.value.field_errors[0]
|
||||
|
||||
|
||||
def test_calculate_processing_opex_extends_evaluation_horizon():
|
||||
request = ProcessingOpexCalculationRequest(
|
||||
def test_calculate_opex_extends_evaluation_horizon():
|
||||
request = OpexCalculationRequest(
|
||||
components=[
|
||||
_component(period_start=1, period_end=4),
|
||||
],
|
||||
parameters=ProcessingOpexParameters(
|
||||
parameters=OpexParameters(
|
||||
currency_code="USD",
|
||||
discount_rate_pct=0,
|
||||
escalation_pct=0,
|
||||
@@ -149,7 +149,7 @@ def test_calculate_processing_opex_extends_evaluation_horizon():
|
||||
),
|
||||
)
|
||||
|
||||
result = calculate_processing_opex(request)
|
||||
result = calculate_opex(request)
|
||||
|
||||
assert len(result.timeline) == 4
|
||||
assert result.timeline[-1].period == 4
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from models import MiningOperationType
|
||||
|
||||
|
||||
class TestDashboardRoute:
|
||||
def test_renders_empty_state(self, client: TestClient) -> None:
|
||||
@@ -17,9 +19,18 @@ class TestDashboardRoute:
|
||||
|
||||
class TestProjectUIRoutes:
|
||||
def test_projects_ui_page_resolves(self, client: TestClient) -> None:
|
||||
create_payload = {
|
||||
"name": "UI Project",
|
||||
"location": "Peru",
|
||||
"operation_type": MiningOperationType.OPEN_PIT.value,
|
||||
"description": "Project for UI validation",
|
||||
}
|
||||
client.post("/projects", json=create_payload)
|
||||
|
||||
response = client.get("/projects/ui")
|
||||
assert response.status_code == 200
|
||||
assert "Projects" in response.text
|
||||
assert "project-card" in response.text
|
||||
|
||||
def test_projects_create_form_resolves(self, client: TestClient) -> None:
|
||||
response = client.get("/projects/create")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from models import MiningOperationType, ResourceType, ScenarioStatus
|
||||
@@ -17,6 +20,32 @@ def _create_project(client: TestClient, name: str = "Alpha Project") -> dict:
|
||||
return response.json()
|
||||
|
||||
|
||||
def _create_scenario(
|
||||
client: TestClient,
|
||||
project_id: int,
|
||||
*,
|
||||
name: str = "Scenario A",
|
||||
status: ScenarioStatus = ScenarioStatus.DRAFT,
|
||||
currency: str | None = "USD",
|
||||
primary_resource: ResourceType | None = ResourceType.DIESEL,
|
||||
) -> dict:
|
||||
payload = {
|
||||
"name": name,
|
||||
"status": status.value,
|
||||
}
|
||||
if currency:
|
||||
payload["currency"] = currency
|
||||
if primary_resource:
|
||||
payload["primary_resource"] = primary_resource.value
|
||||
|
||||
response = client.post(
|
||||
f"/projects/{project_id}/scenarios",
|
||||
json=payload,
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_project_crud_cycle(client: TestClient) -> None:
|
||||
project = _create_project(client)
|
||||
project_id = project["id"]
|
||||
@@ -29,10 +58,13 @@ def test_project_crud_cycle(client: TestClient) -> None:
|
||||
project_ids = {item["id"] for item in list_response.json()}
|
||||
assert project_id in project_ids
|
||||
|
||||
update_payload = {"description": "Updated project description", "location": "Peru"}
|
||||
update_response = client.put(f"/projects/{project_id}", json=update_payload)
|
||||
update_payload = {
|
||||
"description": "Updated project description", "location": "Peru"}
|
||||
update_response = client.put(
|
||||
f"/projects/{project_id}", json=update_payload)
|
||||
assert update_response.status_code == 200
|
||||
assert update_response.json()["description"] == "Updated project description"
|
||||
assert update_response.json(
|
||||
)["description"] == "Updated project description"
|
||||
assert update_response.json()["location"] == "Peru"
|
||||
|
||||
delete_response = client.delete(f"/projects/{project_id}")
|
||||
@@ -97,8 +129,10 @@ def test_scenario_crud_cycle(client: TestClient) -> None:
|
||||
listed_ids = {item["id"] for item in list_response.json()}
|
||||
assert scenario_id in listed_ids
|
||||
|
||||
update_payload = {"description": "Revised assumptions", "status": ScenarioStatus.ACTIVE.value}
|
||||
update_response = client.put(f"/scenarios/{scenario_id}", json=update_payload)
|
||||
update_payload = {"description": "Revised assumptions",
|
||||
"status": ScenarioStatus.ACTIVE.value}
|
||||
update_response = client.put(
|
||||
f"/scenarios/{scenario_id}", json=update_payload)
|
||||
assert update_response.status_code == 200
|
||||
updated = update_response.json()
|
||||
assert updated["description"] == "Revised assumptions"
|
||||
@@ -125,10 +159,12 @@ def test_create_scenario_conflict_returns_409(client: TestClient) -> None:
|
||||
project_id = project["id"]
|
||||
|
||||
payload = {"name": "Duplicate Scenario"}
|
||||
first_response = client.post(f"/projects/{project_id}/scenarios", json=payload)
|
||||
first_response = client.post(
|
||||
f"/projects/{project_id}/scenarios", json=payload)
|
||||
assert first_response.status_code == 201
|
||||
|
||||
conflict_response = client.post(f"/projects/{project_id}/scenarios", json=payload)
|
||||
conflict_response = client.post(
|
||||
f"/projects/{project_id}/scenarios", json=payload)
|
||||
assert conflict_response.status_code == 409
|
||||
assert "constraints" in conflict_response.json()["detail"].lower()
|
||||
|
||||
@@ -150,3 +186,152 @@ def test_create_scenario_invalid_currency_returns_422(client: TestClient) -> Non
|
||||
def test_list_scenarios_missing_project_returns_404(client: TestClient) -> None:
|
||||
response = client.get("/projects/424242/scenarios")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_project_detail_page_renders_scenario_list(client: TestClient) -> None:
|
||||
project = _create_project(client, name="UI Detail Project")
|
||||
project_id = project["id"]
|
||||
|
||||
_create_scenario(
|
||||
client,
|
||||
project_id,
|
||||
name="Scenario UI",
|
||||
status=ScenarioStatus.ACTIVE,
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
response = client.get(f"/projects/{project_id}/view")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
assert "scenario-list" in body
|
||||
assert "status-pill--active" in body
|
||||
|
||||
|
||||
def test_scenario_list_page_shows_calculator_shortcuts(client: TestClient) -> None:
|
||||
project = _create_project(client, name="Portfolio Project")
|
||||
project_id = project["id"]
|
||||
scenario = _create_scenario(
|
||||
client,
|
||||
project_id,
|
||||
name="Portfolio Scenario",
|
||||
status=ScenarioStatus.ACTIVE,
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
response = client.get(f"/projects/{project_id}/scenarios/ui")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
unescaped = html.unescape(body)
|
||||
|
||||
assert "Scenario Portfolio" in body
|
||||
assert project["name"] in body
|
||||
assert scenario["name"] in body
|
||||
assert f"projects/{project_id}/view" in unescaped
|
||||
assert f"scenarios/{scenario['id']}/view" in unescaped
|
||||
expected_calc_fragment = (
|
||||
f"calculations/projects/{project_id}/scenarios/{scenario['id']}/profitability"
|
||||
)
|
||||
assert expected_calc_fragment in unescaped
|
||||
|
||||
|
||||
def test_scenario_detail_page_links_back_to_portfolio(client: TestClient) -> None:
|
||||
project = _create_project(client, name="Detail Project")
|
||||
scenario = _create_scenario(
|
||||
client,
|
||||
project["id"],
|
||||
name="Detail Scenario",
|
||||
status=ScenarioStatus.ACTIVE,
|
||||
currency="USD",
|
||||
primary_resource=ResourceType.ELECTRICITY,
|
||||
)
|
||||
|
||||
response = client.get(f"/scenarios/{scenario['id']}/view")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
unescaped = html.unescape(body)
|
||||
|
||||
assert project["name"] in body
|
||||
assert scenario["name"] in body
|
||||
assert "Scenario Overview" in body
|
||||
assert f"projects/{project['id']}/scenarios/ui" in unescaped
|
||||
assert (
|
||||
f"calculations/projects/{project['id']}/scenarios/{scenario['id']}/profitability"
|
||||
in unescaped
|
||||
)
|
||||
|
||||
|
||||
def test_scenario_form_includes_project_context_guidance(client: TestClient) -> None:
|
||||
project = _create_project(client, name="Form Project")
|
||||
|
||||
response = client.get(f"/projects/{project['id']}/scenarios/new")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
|
||||
assert "Project Context" in body
|
||||
assert project["name"] in body
|
||||
assert "Status Guidance" in body
|
||||
assert "Baseline Reminder" in body
|
||||
assert "Defaults to" in body
|
||||
|
||||
|
||||
def test_calculator_headers_surface_scenario_navigation(client: TestClient) -> None:
|
||||
project = _create_project(client, name="Calc Project")
|
||||
scenario = _create_scenario(
|
||||
client,
|
||||
project["id"],
|
||||
name="Calc Scenario",
|
||||
status=ScenarioStatus.DRAFT,
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
profitability = client.get(
|
||||
f"/calculations/projects/{project['id']}/scenarios/{scenario['id']}/profitability"
|
||||
)
|
||||
assert profitability.status_code == 200
|
||||
profitability_body = html.unescape(profitability.text)
|
||||
assert f"scenarios/{scenario['id']}/view" in profitability_body
|
||||
assert f"projects/{project['id']}/scenarios/ui" in profitability_body
|
||||
assert (
|
||||
f"calculations/projects/{project['id']}/scenarios/{scenario['id']}/profitability"
|
||||
in profitability_body
|
||||
)
|
||||
|
||||
capex = client.get(
|
||||
f"/calculations/capex?project_id={project['id']}&scenario_id={scenario['id']}"
|
||||
)
|
||||
assert capex.status_code == 200
|
||||
capex_body = html.unescape(capex.text)
|
||||
assert f"scenarios/{scenario['id']}/view" in capex_body
|
||||
assert f"projects/{project['id']}/scenarios/ui" in capex_body
|
||||
|
||||
opex = client.get(
|
||||
f"/calculations/opex?project_id={project['id']}&scenario_id={scenario['id']}"
|
||||
)
|
||||
assert opex.status_code == 200
|
||||
opex_body = html.unescape(opex.text)
|
||||
assert f"scenarios/{scenario['id']}/view" in opex_body
|
||||
assert f"projects/{project['id']}/scenarios/ui" in opex_body
|
||||
|
||||
|
||||
def test_profitability_legacy_endpoint_redirects_to_scenario_path(client: TestClient) -> None:
|
||||
project = _create_project(client, name="Redirect Project")
|
||||
scenario = _create_scenario(
|
||||
client,
|
||||
project["id"],
|
||||
name="Redirect Scenario",
|
||||
status=ScenarioStatus.ACTIVE,
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f"/calculations/profitability?project_id={project['id']}&scenario_id={scenario['id']}",
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_307_TEMPORARY_REDIRECT
|
||||
location = response.headers.get("location")
|
||||
assert location is not None
|
||||
expected_path = (
|
||||
f"/calculations/projects/{project['id']}/scenarios/{scenario['id']}/profitability"
|
||||
)
|
||||
assert expected_path in location
|
||||
|
||||
Reference in New Issue
Block a user