feat: add integration tests for project and scenario lifecycles, update templates to new Starlette signature, and optimize project retrieval logic
This commit is contained in:
@@ -14,3 +14,5 @@
|
|||||||
- Extended repositories with count/recency utilities and added pytest coverage, including a dashboard rendering smoke test validating empty-state messaging.
|
- Extended repositories with count/recency utilities and added pytest coverage, including a dashboard rendering smoke test validating empty-state messaging.
|
||||||
- Brought project and scenario detail pages plus their forms in line with the dashboard visuals, adding metric cards, layout grids, and refreshed CTA styles.
|
- Brought project and scenario detail pages plus their forms in line with the dashboard visuals, adding metric cards, layout grids, and refreshed CTA styles.
|
||||||
- Reordered project route registration to prioritize static UI paths, eliminating 422 errors on `/projects/ui` and `/projects/create`, and added pytest smoke coverage for the navigation endpoints.
|
- Reordered project route registration to prioritize static UI paths, eliminating 422 errors on `/projects/ui` and `/projects/create`, and added pytest smoke coverage for the navigation endpoints.
|
||||||
|
- Added end-to-end integration tests for project and scenario lifecycles, validating HTML redirects, template rendering, and API interactions, and updated `ProjectRepository.get` to deduplicate joined loads for detail views.
|
||||||
|
- Updated all Jinja2 template responses to the new Starlette signature to eliminate deprecation warnings while keeping request-aware context available to the templates.
|
||||||
|
|||||||
@@ -105,10 +105,9 @@ def dashboard_home(
|
|||||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
context = {
|
context = {
|
||||||
"request": request,
|
|
||||||
"metrics": _load_metrics(uow),
|
"metrics": _load_metrics(uow),
|
||||||
"recent_projects": _load_recent_projects(uow),
|
"recent_projects": _load_recent_projects(uow),
|
||||||
"simulation_updates": _load_simulation_updates(uow),
|
"simulation_updates": _load_simulation_updates(uow),
|
||||||
"scenario_alerts": _load_scenario_alerts(request, uow),
|
"scenario_alerts": _load_scenario_alerts(request, uow),
|
||||||
}
|
}
|
||||||
return templates.TemplateResponse("dashboard.html", context)
|
return templates.TemplateResponse(request, "dashboard.html", context)
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ def project_list_page(
|
|||||||
for project in projects:
|
for project in projects:
|
||||||
setattr(project, "scenario_count", len(project.scenarios))
|
setattr(project, "scenario_count", len(project.scenarios))
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"projects/list.html",
|
"projects/list.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"projects": projects,
|
"projects": projects,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -75,9 +75,9 @@ def project_list_page(
|
|||||||
)
|
)
|
||||||
def create_project_form(request: Request) -> HTMLResponse:
|
def create_project_form(request: Request) -> HTMLResponse:
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"projects/form.html",
|
"projects/form.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": None,
|
"project": None,
|
||||||
"operation_types": _operation_type_choices(),
|
"operation_types": _operation_type_choices(),
|
||||||
"form_action": request.url_for("projects.create_project_submit"),
|
"form_action": request.url_for("projects.create_project_submit"),
|
||||||
@@ -109,9 +109,9 @@ def create_project_submit(
|
|||||||
op_type = MiningOperationType(operation_type)
|
op_type = MiningOperationType(operation_type)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"projects/form.html",
|
"projects/form.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": None,
|
"project": None,
|
||||||
"operation_types": _operation_type_choices(),
|
"operation_types": _operation_type_choices(),
|
||||||
"form_action": request.url_for("projects.create_project_submit"),
|
"form_action": request.url_for("projects.create_project_submit"),
|
||||||
@@ -131,9 +131,9 @@ def create_project_submit(
|
|||||||
uow.projects.create(project)
|
uow.projects.create(project)
|
||||||
except EntityConflictError as exc:
|
except EntityConflictError as exc:
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"projects/form.html",
|
"projects/form.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": project,
|
"project": project,
|
||||||
"operation_types": _operation_type_choices(),
|
"operation_types": _operation_type_choices(),
|
||||||
"form_action": request.url_for("projects.create_project_submit"),
|
"form_action": request.url_for("projects.create_project_submit"),
|
||||||
@@ -219,9 +219,9 @@ def view_project(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"projects/detail.html",
|
"projects/detail.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": project,
|
"project": project,
|
||||||
"scenarios": scenarios,
|
"scenarios": scenarios,
|
||||||
"scenario_stats": scenario_stats,
|
"scenario_stats": scenario_stats,
|
||||||
@@ -246,9 +246,9 @@ def edit_project_form(
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"projects/form.html",
|
"projects/form.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": project,
|
"project": project,
|
||||||
"operation_types": _operation_type_choices(),
|
"operation_types": _operation_type_choices(),
|
||||||
"form_action": request.url_for(
|
"form_action": request.url_for(
|
||||||
@@ -295,9 +295,9 @@ def edit_project_submit(
|
|||||||
project.operation_type = MiningOperationType(operation_type)
|
project.operation_type = MiningOperationType(operation_type)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"projects/form.html",
|
"projects/form.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": project,
|
"project": project,
|
||||||
"operation_types": _operation_type_choices(),
|
"operation_types": _operation_type_choices(),
|
||||||
"form_action": request.url_for(
|
"form_action": request.url_for(
|
||||||
|
|||||||
@@ -218,9 +218,9 @@ def create_scenario_form(
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"scenarios/form.html",
|
"scenarios/form.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": project,
|
"project": project,
|
||||||
"scenario": None,
|
"scenario": None,
|
||||||
"scenario_statuses": _scenario_status_choices(),
|
"scenario_statuses": _scenario_status_choices(),
|
||||||
@@ -291,9 +291,9 @@ def create_scenario_submit(
|
|||||||
uow.scenarios.create(scenario)
|
uow.scenarios.create(scenario)
|
||||||
except EntityConflictError as exc:
|
except EntityConflictError as exc:
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"scenarios/form.html",
|
"scenarios/form.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": project,
|
"project": project,
|
||||||
"scenario": scenario,
|
"scenario": scenario,
|
||||||
"scenario_statuses": _scenario_status_choices(),
|
"scenario_statuses": _scenario_status_choices(),
|
||||||
@@ -347,9 +347,9 @@ def view_scenario(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"scenarios/detail.html",
|
"scenarios/detail.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": project,
|
"project": project,
|
||||||
"scenario": scenario,
|
"scenario": scenario,
|
||||||
"scenario_metrics": scenario_metrics,
|
"scenario_metrics": scenario_metrics,
|
||||||
@@ -378,9 +378,9 @@ def edit_scenario_form(
|
|||||||
project = uow.projects.get(scenario.project_id)
|
project = uow.projects.get(scenario.project_id)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"scenarios/form.html",
|
"scenarios/form.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
|
||||||
"project": project,
|
"project": project,
|
||||||
"scenario": scenario,
|
"scenario": scenario,
|
||||||
"scenario_statuses": _scenario_status_choices(),
|
"scenario_statuses": _scenario_status_choices(),
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ class ProjectRepository:
|
|||||||
stmt = select(Project).where(Project.id == project_id)
|
stmt = select(Project).where(Project.id == project_id)
|
||||||
if with_children:
|
if with_children:
|
||||||
stmt = stmt.options(joinedload(Project.scenarios))
|
stmt = stmt.options(joinedload(Project.scenarios))
|
||||||
project = self.session.execute(stmt).scalar_one_or_none()
|
result = self.session.execute(stmt)
|
||||||
|
if with_children:
|
||||||
|
result = result.unique()
|
||||||
|
project = result.scalar_one_or_none()
|
||||||
if project is None:
|
if project is None:
|
||||||
raise EntityNotFoundError(f"Project {project_id} not found")
|
raise EntityNotFoundError(f"Project {project_id} not found")
|
||||||
return project
|
return project
|
||||||
|
|||||||
72
tests/conftest.py
Normal file
72
tests/conftest.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Iterator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from config.database import Base
|
||||||
|
from dependencies import get_unit_of_work
|
||||||
|
from routes.dashboard import router as dashboard_router
|
||||||
|
from routes.projects import router as projects_router
|
||||||
|
from routes.scenarios import router as scenarios_router
|
||||||
|
from services.unit_of_work import UnitOfWork
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def engine() -> Iterator[Engine]:
|
||||||
|
engine = create_engine(
|
||||||
|
"sqlite+pysqlite:///:memory:",
|
||||||
|
future=True,
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
poolclass=StaticPool,
|
||||||
|
)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
try:
|
||||||
|
yield engine
|
||||||
|
finally:
|
||||||
|
Base.metadata.drop_all(bind=engine)
|
||||||
|
engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def session_factory(engine: Engine) -> Iterator[sessionmaker]:
|
||||||
|
testing_session = sessionmaker(bind=engine, expire_on_commit=False, future=True)
|
||||||
|
yield testing_session
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def app(session_factory: sessionmaker) -> FastAPI:
|
||||||
|
application = FastAPI()
|
||||||
|
application.include_router(dashboard_router)
|
||||||
|
application.include_router(projects_router)
|
||||||
|
application.include_router(scenarios_router)
|
||||||
|
|
||||||
|
def _override_uow() -> Iterator[UnitOfWork]:
|
||||||
|
with UnitOfWork(session_factory=session_factory) as uow:
|
||||||
|
yield uow
|
||||||
|
|
||||||
|
application.dependency_overrides[get_unit_of_work] = _override_uow
|
||||||
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client(app: FastAPI) -> Iterator[TestClient]:
|
||||||
|
test_client = TestClient(app)
|
||||||
|
try:
|
||||||
|
yield test_client
|
||||||
|
finally:
|
||||||
|
test_client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def unit_of_work_factory(session_factory: sessionmaker) -> Callable[[], UnitOfWork]:
|
||||||
|
def _factory() -> UnitOfWork:
|
||||||
|
return UnitOfWork(session_factory=session_factory)
|
||||||
|
|
||||||
|
return _factory
|
||||||
30
tests/integration/README.md
Normal file
30
tests/integration/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Lifecycle Integration Test Plan
|
||||||
|
|
||||||
|
## Coverage Goals
|
||||||
|
|
||||||
|
- Exercise end-to-end creation, update, and deletion flows for projects through both API endpoints and HTML form submissions to ensure consistency across interfaces.
|
||||||
|
- Validate scenario lifecycle interactions (create, update, archive) including business rule enforcement and status transitions when performed via API and UI routes.
|
||||||
|
- Confirm that redirect chains and rendered templates match expected destinations after each lifecycle operation.
|
||||||
|
- Verify repository-backed statistics (counts, recency metadata) update appropriately after lifecycle actions to maintain dashboard accuracy.
|
||||||
|
- Guard against regressions in unit-of-work transaction handling by asserting database state after success and failure paths within integration flows.
|
||||||
|
|
||||||
|
## Happy-Path Journeys
|
||||||
|
|
||||||
|
1. **Project Management Flow**
|
||||||
|
|
||||||
|
- Navigate to the project list UI and confirm empty-state messaging.
|
||||||
|
- Submit the new project form with valid data and verify redirect to the list page with the project present.
|
||||||
|
- Perform an API-based update to adjust project metadata and check the UI detail view reflects changes.
|
||||||
|
- Delete the project through the API and ensure the list UI reverts to the empty state.
|
||||||
|
|
||||||
|
2. **Scenario Lifecycle Flow**
|
||||||
|
|
||||||
|
- From an existing project, create a new scenario via the API and verify the project detail page renders the scenario entry.
|
||||||
|
- Update the scenario through the UI form, ensuring redirect to the scenario detail view with updated fields.
|
||||||
|
- Trigger a validation rule (e.g., duplicate scenario name within a project) to confirm error messaging without data loss.
|
||||||
|
- Archive the scenario using the API and confirm status badges and scenario counts update across dashboard and project detail views.
|
||||||
|
|
||||||
|
3. **Dashboard Consistency Flow**
|
||||||
|
- Seed multiple projects and scenarios through combined API/UI interactions.
|
||||||
|
- Visit the dashboard and ensure metric cards reflect the latest counts, active/draft status breakdowns, and recent activity timestamps after each mutation.
|
||||||
|
- Run the lifecycle flows sequentially to confirm cumulative effects propagate to dashboard summaries and navigation badges.
|
||||||
66
tests/integration/test_project_lifecycle.py
Normal file
66
tests/integration/test_project_lifecycle.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectLifecycle:
|
||||||
|
def test_project_create_update_delete_flow(self, client: TestClient) -> None:
|
||||||
|
# Initial state: no projects listed on the UI page
|
||||||
|
response = client.get("/projects/ui")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "No projects yet" in response.text
|
||||||
|
|
||||||
|
# Create a project via the HTML form submission
|
||||||
|
create_payload = {
|
||||||
|
"name": "Lifecycle Mine",
|
||||||
|
"location": "Nevada",
|
||||||
|
"operation_type": "open_pit",
|
||||||
|
"description": "Initial description",
|
||||||
|
}
|
||||||
|
response = client.post(
|
||||||
|
"/projects/create",
|
||||||
|
data=create_payload,
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert response.status_code == 303
|
||||||
|
assert response.headers["Location"].endswith("/projects/ui")
|
||||||
|
|
||||||
|
# Project should now appear on the list page
|
||||||
|
response = client.get("/projects/ui")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Lifecycle Mine" in response.text
|
||||||
|
assert "Nevada" in response.text
|
||||||
|
|
||||||
|
# Fetch the project via API to obtain its identifier
|
||||||
|
response = client.get("/projects")
|
||||||
|
assert response.status_code == 200
|
||||||
|
projects = response.json()
|
||||||
|
assert len(projects) == 1
|
||||||
|
project_id = projects[0]["id"]
|
||||||
|
|
||||||
|
# Update the project using the API endpoint
|
||||||
|
update_payload = {
|
||||||
|
"location": "Arizona",
|
||||||
|
"description": "Updated description",
|
||||||
|
}
|
||||||
|
response = client.put(f"/projects/{project_id}", json=update_payload)
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.json()
|
||||||
|
assert body["location"] == "Arizona"
|
||||||
|
assert body["description"] == "Updated description"
|
||||||
|
|
||||||
|
# Verify the UI detail page reflects the updates
|
||||||
|
response = client.get(f"/projects/{project_id}/view")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Arizona" in response.text
|
||||||
|
assert "Updated description" in response.text
|
||||||
|
|
||||||
|
# Delete the project using the API endpoint
|
||||||
|
response = client.delete(f"/projects/{project_id}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
# Ensure the list view returns to the empty state
|
||||||
|
response = client.get("/projects/ui")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "No projects yet" in response.text
|
||||||
|
assert "Lifecycle Mine" not in response.text
|
||||||
106
tests/integration/test_scenario_lifecycle.py
Normal file
106
tests/integration/test_scenario_lifecycle.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestScenarioLifecycle:
|
||||||
|
def test_scenario_lifecycle_flow(self, client: TestClient) -> None:
|
||||||
|
# Create a project to attach scenarios to
|
||||||
|
project_response = client.post(
|
||||||
|
"/projects",
|
||||||
|
json={
|
||||||
|
"name": "Scenario Host Project",
|
||||||
|
"location": "Ontario",
|
||||||
|
"operation_type": "open_pit",
|
||||||
|
"description": "Project for scenario lifecycle testing",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert project_response.status_code == 201
|
||||||
|
project_id = project_response.json()["id"]
|
||||||
|
|
||||||
|
# Create a scenario via the API for the project
|
||||||
|
scenario_response = client.post(
|
||||||
|
f"/projects/{project_id}/scenarios",
|
||||||
|
json={
|
||||||
|
"name": "Lifecycle Scenario",
|
||||||
|
"description": "Initial scenario description",
|
||||||
|
"status": "draft",
|
||||||
|
"currency": "usd",
|
||||||
|
"primary_resource": "diesel",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert scenario_response.status_code == 201
|
||||||
|
scenario_id = scenario_response.json()["id"]
|
||||||
|
|
||||||
|
# Project detail page should list the new scenario in draft state
|
||||||
|
project_detail = client.get(f"/projects/{project_id}/view")
|
||||||
|
assert project_detail.status_code == 200
|
||||||
|
assert "Lifecycle Scenario" in project_detail.text
|
||||||
|
assert "<td>Draft</td>" in project_detail.text
|
||||||
|
|
||||||
|
# Update the scenario through the HTML form
|
||||||
|
form_response = client.post(
|
||||||
|
f"/scenarios/{scenario_id}/edit",
|
||||||
|
data={
|
||||||
|
"name": "Lifecycle Scenario Revised",
|
||||||
|
"description": "Revised scenario assumptions",
|
||||||
|
"status_value": "active",
|
||||||
|
"start_date": "2025-01-01",
|
||||||
|
"end_date": "2025-12-31",
|
||||||
|
"discount_rate": "5.5",
|
||||||
|
"currency": "cad",
|
||||||
|
"primary_resource": "electricity",
|
||||||
|
},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert form_response.status_code == 303
|
||||||
|
assert form_response.headers["Location"].endswith(
|
||||||
|
f"/scenarios/{scenario_id}/view")
|
||||||
|
|
||||||
|
# Scenario detail page should reflect the updated information
|
||||||
|
scenario_detail = client.get(f"/scenarios/{scenario_id}/view")
|
||||||
|
assert scenario_detail.status_code == 200
|
||||||
|
assert "Lifecycle Scenario Revised" in scenario_detail.text
|
||||||
|
assert "Status: Active" in scenario_detail.text
|
||||||
|
assert "CAD" in scenario_detail.text
|
||||||
|
assert "Electricity" in scenario_detail.text
|
||||||
|
assert "Revised scenario assumptions" in scenario_detail.text
|
||||||
|
|
||||||
|
# Project detail page should show the scenario as active with updated currency/resource
|
||||||
|
project_detail = client.get(f"/projects/{project_id}/view")
|
||||||
|
assert "<td>Active</td>" in project_detail.text
|
||||||
|
assert "<td>CAD</td>" in project_detail.text
|
||||||
|
assert "<td>Electricity</td>" in project_detail.text
|
||||||
|
|
||||||
|
# Attempt to update the scenario with invalid currency to trigger validation error
|
||||||
|
invalid_update = client.put(
|
||||||
|
f"/scenarios/{scenario_id}",
|
||||||
|
json={"currency": "ca"},
|
||||||
|
)
|
||||||
|
assert invalid_update.status_code == 422
|
||||||
|
assert (
|
||||||
|
invalid_update.json()["detail"][0]["msg"]
|
||||||
|
== "Value error, Currency code must be a 3-letter ISO value"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Scenario detail should still show the previous (valid) currency
|
||||||
|
scenario_detail = client.get(f"/scenarios/{scenario_id}/view")
|
||||||
|
assert "CAD" in scenario_detail.text
|
||||||
|
|
||||||
|
# Archive the scenario through the API
|
||||||
|
archive_response = client.put(
|
||||||
|
f"/scenarios/{scenario_id}",
|
||||||
|
json={"status": "archived"},
|
||||||
|
)
|
||||||
|
assert archive_response.status_code == 200
|
||||||
|
assert archive_response.json()["status"] == "archived"
|
||||||
|
|
||||||
|
# Scenario detail reflects archived status
|
||||||
|
scenario_detail = client.get(f"/scenarios/{scenario_id}/view")
|
||||||
|
assert "Status: Archived" in scenario_detail.text
|
||||||
|
|
||||||
|
# Project detail metrics and table entries reflect the archived state
|
||||||
|
project_detail = client.get(f"/projects/{project_id}/view")
|
||||||
|
assert "<h2>Archived</h2>" in project_detail.text
|
||||||
|
assert '<p class="metric-value">1</p>' in project_detail.text
|
||||||
|
assert "<td>Archived</td>" in project_detail.text
|
||||||
@@ -1,64 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterator
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.engine import Engine
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from sqlalchemy.pool import StaticPool
|
|
||||||
|
|
||||||
from config.database import Base
|
|
||||||
from dependencies import get_unit_of_work
|
|
||||||
from routes.dashboard import router as dashboard_router
|
|
||||||
from routes.projects import router as projects_router
|
|
||||||
from routes.scenarios import router as scenarios_router
|
|
||||||
from services.unit_of_work import UnitOfWork
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def engine() -> Iterator[Engine]:
|
|
||||||
engine = create_engine(
|
|
||||||
"sqlite+pysqlite:///:memory:",
|
|
||||||
future=True,
|
|
||||||
connect_args={"check_same_thread": False},
|
|
||||||
poolclass=StaticPool,
|
|
||||||
)
|
|
||||||
Base.metadata.create_all(bind=engine)
|
|
||||||
try:
|
|
||||||
yield engine
|
|
||||||
finally:
|
|
||||||
Base.metadata.drop_all(bind=engine)
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def session_factory(engine: Engine) -> Iterator[sessionmaker]:
|
|
||||||
testing_session = sessionmaker(
|
|
||||||
bind=engine, expire_on_commit=False, future=True)
|
|
||||||
yield testing_session
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def client(session_factory: sessionmaker) -> Iterator[TestClient]:
|
|
||||||
app = FastAPI()
|
|
||||||
app.include_router(dashboard_router)
|
|
||||||
app.include_router(projects_router)
|
|
||||||
app.include_router(scenarios_router)
|
|
||||||
|
|
||||||
def _override_uow() -> Iterator[UnitOfWork]:
|
|
||||||
with UnitOfWork(session_factory=session_factory) as uow:
|
|
||||||
yield uow
|
|
||||||
|
|
||||||
app.dependency_overrides[get_unit_of_work] = _override_uow
|
|
||||||
|
|
||||||
test_client = TestClient(app)
|
|
||||||
try:
|
|
||||||
yield test_client
|
|
||||||
finally:
|
|
||||||
test_client.close()
|
|
||||||
|
|
||||||
|
|
||||||
class TestDashboardRoute:
|
class TestDashboardRoute:
|
||||||
|
|||||||
Reference in New Issue
Block a user