Refactor test cases for improved readability and consistency
Some checks failed
Run Tests / e2e tests (push) Failing after 1m27s
Run Tests / lint tests (push) Failing after 6s
Run Tests / unit tests (push) Failing after 7s

- Updated test functions in various test files to enhance code clarity by formatting long lines and improving indentation.
- Adjusted assertions to use multi-line formatting for better readability.
- Added new test cases for theme settings API to ensure proper functionality.
- Ensured consistent use of line breaks and spacing across test files for uniformity.
This commit is contained in:
2025-10-27 10:32:55 +01:00
parent e8a86b15e4
commit 97b1c0360b
78 changed files with 2327 additions and 650 deletions

View File

@@ -4,6 +4,7 @@ import time
from typing import Dict, Generator
import pytest
# type: ignore[import]
from playwright.sync_api import Browser, Page, Playwright, sync_playwright
@@ -70,10 +71,17 @@ def seed_default_currencies(live_server: str) -> None:
seeds = [
{"code": "EUR", "name": "Euro", "symbol": "EUR", "is_active": True},
{"code": "CLP", "name": "Chilean Peso", "symbol": "CLP$", "is_active": True},
{
"code": "CLP",
"name": "Chilean Peso",
"symbol": "CLP$",
"is_active": True,
},
]
with httpx.Client(base_url=live_server, timeout=5.0, trust_env=False) as client:
with httpx.Client(
base_url=live_server, timeout=5.0, trust_env=False
) as client:
try:
response = client.get("/api/currencies/?include_inactive=true")
response.raise_for_status()
@@ -128,8 +136,12 @@ def page(browser: Browser, live_server: str) -> Generator[Page, None, None]:
def _prepare_database_environment(env: Dict[str, str]) -> Dict[str, str]:
"""Ensure granular database env vars are available for the app under test."""
required = ("DATABASE_HOST", "DATABASE_USER",
"DATABASE_NAME", "DATABASE_PASSWORD")
required = (
"DATABASE_HOST",
"DATABASE_USER",
"DATABASE_NAME",
"DATABASE_PASSWORD",
)
if all(env.get(key) for key in required):
return env

View File

@@ -7,7 +7,9 @@ def test_consumption_form_loads(page: Page):
"""Verify the consumption form page loads correctly."""
page.goto("/ui/consumption")
expect(page).to_have_title("Consumption · CalMiner")
expect(page.locator("h2:has-text('Add Consumption Record')")).to_be_visible()
expect(
page.locator("h2:has-text('Add Consumption Record')")
).to_be_visible()
def test_create_consumption_item(page: Page):

View File

@@ -55,7 +55,9 @@ def test_create_capex_and_opex_items(page: Page):
).to_be_visible()
# Verify the feedback messages.
expect(page.locator("#capex-feedback")
).to_have_text("Entry saved successfully.")
expect(page.locator("#opex-feedback")
).to_have_text("Entry saved successfully.")
expect(page.locator("#capex-feedback")).to_have_text(
"Entry saved successfully."
)
expect(page.locator("#opex-feedback")).to_have_text(
"Entry saved successfully."
)

View File

@@ -12,7 +12,8 @@ def _unique_currency_code(existing: set[str]) -> str:
if candidate not in existing and candidate != "USD":
return candidate
raise AssertionError(
"Unable to generate a unique currency code for the test run.")
"Unable to generate a unique currency code for the test run."
)
def _metric_value(page: Page, element_id: str) -> int:
@@ -42,8 +43,9 @@ def test_currency_workflow_create_update_toggle(page: Page) -> None:
expect(page.locator("h2:has-text('Currency Overview')")).to_be_visible()
code_cells = page.locator("#currencies-table-body tr td:nth-child(1)")
existing_codes = {text.strip().upper()
for text in code_cells.all_inner_texts()}
existing_codes = {
text.strip().upper() for text in code_cells.all_inner_texts()
}
total_before = _metric_value(page, "currency-metric-total")
active_before = _metric_value(page, "currency-metric-active")
@@ -109,7 +111,9 @@ def test_currency_workflow_create_update_toggle(page: Page) -> None:
toggle_button = row.locator("button[data-action='toggle']")
expect(toggle_button).to_have_text("Activate")
with page.expect_response(f"**/api/currencies/{new_code}/activation") as toggle_info:
with page.expect_response(
f"**/api/currencies/{new_code}/activation"
) as toggle_info:
toggle_button.click()
toggle_response = toggle_info.value
assert toggle_response.status == 200
@@ -126,5 +130,6 @@ def test_currency_workflow_create_update_toggle(page: Page) -> None:
_expect_feedback(page, f"Currency {new_code} activated.")
expect(row.locator("td").nth(3)).to_contain_text("Active")
expect(row.locator("button[data-action='toggle']")
).to_have_text("Deactivate")
expect(row.locator("button[data-action='toggle']")).to_have_text(
"Deactivate"
)

View File

@@ -38,11 +38,8 @@ def test_create_equipment_item(page: Page):
# Verify the new item appears in the table.
page.select_option("#equipment-scenario-filter", label=scenario_name)
expect(
page.locator("#equipment-table-body tr").filter(
has_text=equipment_name
)
page.locator("#equipment-table-body tr").filter(has_text=equipment_name)
).to_be_visible()
# Verify the feedback message.
expect(page.locator("#equipment-feedback")
).to_have_text("Equipment saved.")
expect(page.locator("#equipment-feedback")).to_have_text("Equipment saved.")

View File

@@ -53,5 +53,6 @@ def test_create_maintenance_item(page: Page):
).to_be_visible()
# Verify the feedback message.
expect(page.locator("#maintenance-feedback")
).to_have_text("Maintenance entry saved.")
expect(page.locator("#maintenance-feedback")).to_have_text(
"Maintenance entry saved."
)

View File

@@ -43,5 +43,6 @@ def test_create_production_item(page: Page):
).to_be_visible()
# Verify the feedback message.
expect(page.locator("#production-feedback")
).to_have_text("Production output saved.")
expect(page.locator("#production-feedback")).to_have_text(
"Production output saved."
)

View File

@@ -39,4 +39,5 @@ def test_create_new_scenario(page: Page):
feedback = page.locator("#feedback")
expect(feedback).to_be_visible()
expect(feedback).to_have_text(
f'Scenario "{scenario_name}" created successfully.')
f'Scenario "{scenario_name}" created successfully.'
)

View File

@@ -5,7 +5,11 @@ from playwright.sync_api import Page, expect
UI_ROUTES = [
("/", "Dashboard · CalMiner", "Operations Overview"),
("/ui/dashboard", "Dashboard · CalMiner", "Operations Overview"),
("/ui/scenarios", "Scenario Management · CalMiner", "Create a New Scenario"),
(
"/ui/scenarios",
"Scenario Management · CalMiner",
"Create a New Scenario",
),
("/ui/parameters", "Process Parameters · CalMiner", "Scenario Parameters"),
("/ui/settings", "Settings · CalMiner", "Settings"),
("/ui/costs", "Costs · CalMiner", "Cost Overview"),
@@ -20,35 +24,44 @@ UI_ROUTES = [
@pytest.mark.parametrize("url, title, heading", UI_ROUTES)
def test_ui_pages_load_correctly(page: Page, url: str, title: str, heading: str):
def test_ui_pages_load_correctly(
page: Page, url: str, title: str, heading: str
):
"""Verify that all UI pages load with the correct title and a visible heading."""
page.goto(url)
expect(page).to_have_title(title)
# The app uses a mix of h1 and h2 for main page headings.
heading_locator = page.locator(
f"h1:has-text('{heading}'), h2:has-text('{heading}')")
f"h1:has-text('{heading}'), h2:has-text('{heading}')"
)
expect(heading_locator.first).to_be_visible()
def test_settings_theme_form_interaction(page: Page):
page.goto("/ui/settings")
expect(page).to_have_title("Settings · CalMiner")
page.goto("/theme-settings")
expect(page).to_have_title("Theme Settings · CalMiner")
env_rows = page.locator("#theme-env-overrides tbody tr")
disabled_inputs = page.locator(
"#theme-settings-form input.color-value-input[disabled]")
"#theme-settings-form input.color-value-input[disabled]"
)
env_row_count = env_rows.count()
disabled_count = disabled_inputs.count()
assert disabled_count == env_row_count
color_input = page.locator(
"#theme-settings-form input[name='--color-primary']")
"#theme-settings-form input[name='--color-primary']"
)
expect(color_input).to_be_visible()
expect(color_input).to_be_enabled()
original_value = color_input.input_value()
candidate_values = ("#114455", "#225566")
new_value = candidate_values[0] if original_value != candidate_values[0] else candidate_values[1]
new_value = (
candidate_values[0]
if original_value != candidate_values[0]
else candidate_values[1]
)
color_input.fill(new_value)
page.click("#theme-settings-form button[type='submit']")

View File

@@ -27,7 +27,8 @@ engine = create_engine(
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine)
autocommit=False, autoflush=False, bind=engine
)
@pytest.fixture(scope="session", autouse=True)
@@ -37,19 +38,24 @@ def setup_database() -> Generator[None, None, None]:
application_setting,
capex,
consumption,
currency,
distribution,
equipment,
maintenance,
opex,
parameters,
production_output,
role,
scenario,
simulation_result,
theme_setting,
user,
) # noqa: F401 - imported for side effects
_ = (
capex,
consumption,
currency,
distribution,
equipment,
maintenance,
@@ -57,8 +63,11 @@ def setup_database() -> Generator[None, None, None]:
opex,
parameters,
production_output,
role,
scenario,
simulation_result,
theme_setting,
user,
)
Base.metadata.create_all(bind=engine)
@@ -86,22 +95,23 @@ def api_client(db_session: Session) -> Generator[TestClient, None, None]:
finally:
pass
from routes import dependencies as route_dependencies
from routes.dependencies import get_db
app.dependency_overrides[route_dependencies.get_db] = override_get_db
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as client:
yield client
app.dependency_overrides.pop(route_dependencies.get_db, None)
app.dependency_overrides.pop(get_db, None)
@pytest.fixture()
def seeded_ui_data(db_session: Session) -> Generator[Dict[str, Any], None, None]:
def seeded_ui_data(
db_session: Session,
) -> Generator[Dict[str, Any], None, None]:
"""Populate a scenario with representative related records for UI tests."""
scenario_name = f"Scenario Alpha {uuid4()}"
scenario = Scenario(name=scenario_name,
description="Seeded UI scenario")
scenario = Scenario(name=scenario_name, description="Seeded UI scenario")
db_session.add(scenario)
db_session.flush()
@@ -161,7 +171,9 @@ def seeded_ui_data(db_session: Session) -> Generator[Dict[str, Any], None, None]
iteration=index,
result=value,
)
for index, value in enumerate((950_000.0, 975_000.0, 990_000.0), start=1)
for index, value in enumerate(
(950_000.0, 975_000.0, 990_000.0), start=1
)
]
db_session.add(maintenance)
@@ -196,11 +208,15 @@ def seeded_ui_data(db_session: Session) -> Generator[Dict[str, Any], None, None]
@pytest.fixture()
def invalid_request_payloads(db_session: Session) -> Generator[Dict[str, Any], None, None]:
def invalid_request_payloads(
db_session: Session,
) -> Generator[Dict[str, Any], None, None]:
"""Provide reusable invalid request bodies for exercising validation branches."""
duplicate_name = f"Scenario Duplicate {uuid4()}"
existing = Scenario(name=duplicate_name,
description="Existing scenario for duplicate checks")
existing = Scenario(
name=duplicate_name,
description="Existing scenario for duplicate checks",
)
db_session.add(existing)
db_session.commit()

231
tests/unit/test_auth.py Normal file
View File

@@ -0,0 +1,231 @@
from services.security import get_password_hash, verify_password
def test_password_hashing():
password = "testpassword"
hashed_password = get_password_hash(password)
assert verify_password(password, hashed_password)
assert not verify_password("wrongpassword", hashed_password)
def test_register_user(api_client):
response = api_client.post(
"/users/register",
json={
"username": "testuser",
"email": "test@example.com",
"password": "testpassword",
},
)
assert response.status_code == 201
data = response.json()
assert data["username"] == "testuser"
assert data["email"] == "test@example.com"
assert "id" in data
assert "role_id" in data
response = api_client.post(
"/users/register",
json={
"username": "testuser",
"email": "another@example.com",
"password": "testpassword",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Username already registered"}
response = api_client.post(
"/users/register",
json={
"username": "anotheruser",
"email": "test@example.com",
"password": "testpassword",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Email already registered"}
def test_login_user(api_client):
# Register a user first
api_client.post(
"/users/register",
json={
"username": "loginuser",
"email": "login@example.com",
"password": "loginpassword",
},
)
response = api_client.post(
"/users/login",
json={"username": "loginuser", "password": "loginpassword"},
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
response = api_client.post(
"/users/login",
json={"username": "loginuser", "password": "wrongpassword"},
)
assert response.status_code == 401
assert response.json() == {"detail": "Incorrect username or password"}
response = api_client.post(
"/users/login",
json={"username": "nonexistent", "password": "password"},
)
assert response.status_code == 401
assert response.json() == {"detail": "Incorrect username or password"}
def test_read_users_me(api_client):
# Register a user first
api_client.post(
"/users/register",
json={
"username": "profileuser",
"email": "profile@example.com",
"password": "profilepassword",
},
)
# Login to get a token
login_response = api_client.post(
"/users/login",
json={"username": "profileuser", "password": "profilepassword"},
)
token = login_response.json()["access_token"]
response = api_client.get(
"/users/me", headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
data = response.json()
assert data["username"] == "profileuser"
assert data["email"] == "profile@example.com"
def test_update_users_me(api_client):
# Register a user first
api_client.post(
"/users/register",
json={
"username": "updateuser",
"email": "update@example.com",
"password": "updatepassword",
},
)
# Login to get a token
login_response = api_client.post(
"/users/login",
json={"username": "updateuser", "password": "updatepassword"},
)
token = login_response.json()["access_token"]
response = api_client.put(
"/users/me",
headers={"Authorization": f"Bearer {token}"},
json={
"username": "updateduser",
"email": "updated@example.com",
"password": "newpassword",
},
)
assert response.status_code == 200
data = response.json()
assert data["username"] == "updateduser"
assert data["email"] == "updated@example.com"
# Verify password change
response = api_client.post(
"/users/login",
json={"username": "updateduser", "password": "newpassword"},
)
assert response.status_code == 200
token = response.json()["access_token"]
# Test username already taken
api_client.post(
"/users/register",
json={
"username": "anotherupdateuser",
"email": "anotherupdate@example.com",
"password": "password",
},
)
response = api_client.put(
"/users/me",
headers={"Authorization": f"Bearer {token}"},
json={
"username": "anotherupdateuser",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Username already taken"}
# Test email already registered
api_client.post(
"/users/register",
json={
"username": "yetanotheruser",
"email": "yetanother@example.com",
"password": "password",
},
)
response = api_client.put(
"/users/me",
headers={"Authorization": f"Bearer {token}"},
json={
"email": "yetanother@example.com",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Email already registered"}
def test_forgot_password(api_client):
response = api_client.post(
"/users/forgot-password", json={"email": "nonexistent@example.com"}
)
assert response.status_code == 200
assert response.json() == {
"message": "Password reset email sent (not really)"}
def test_reset_password(api_client):
# Register a user first
api_client.post(
"/users/register",
json={
"username": "resetuser",
"email": "reset@example.com",
"password": "oldpassword",
},
)
response = api_client.post(
"/users/reset-password",
json={
"token": "resetuser", # Use username as token for test
"new_password": "newpassword",
},
)
assert response.status_code == 200
assert response.json() == {
"message": "Password has been reset successfully"}
# Verify password change
response = api_client.post(
"/users/login",
json={"username": "resetuser", "password": "newpassword"},
)
assert response.status_code == 200
response = api_client.post(
"/users/login",
json={"username": "resetuser", "password": "oldpassword"},
)
assert response.status_code == 401

View File

@@ -57,8 +57,11 @@ def test_list_consumption_returns_created_items(client: TestClient) -> None:
list_response = client.get("/api/consumption/")
assert list_response.status_code == 200
items = [item for item in list_response.json(
) if item["scenario_id"] == scenario_id]
items = [
item
for item in list_response.json()
if item["scenario_id"] == scenario_id
]
assert {item["amount"] for item in items} == set(values)

View File

@@ -47,8 +47,9 @@ def test_create_and_list_capex_and_opex():
resp3 = client.get("/api/costs/capex")
assert resp3.status_code == 200
data = resp3.json()
assert any(item["amount"] == 1000.0 and item["scenario_id"]
== sid for item in data)
assert any(
item["amount"] == 1000.0 and item["scenario_id"] == sid for item in data
)
opex_payload = {
"scenario_id": sid,
@@ -66,8 +67,10 @@ def test_create_and_list_capex_and_opex():
resp5 = client.get("/api/costs/opex")
assert resp5.status_code == 200
data_o = resp5.json()
assert any(item["amount"] == 500.0 and item["scenario_id"]
== sid for item in data_o)
assert any(
item["amount"] == 500.0 and item["scenario_id"] == sid
for item in data_o
)
def test_multiple_capex_entries():
@@ -88,8 +91,9 @@ def test_multiple_capex_entries():
resp = client.get("/api/costs/capex")
assert resp.status_code == 200
data = resp.json()
retrieved_amounts = [item["amount"]
for item in data if item["scenario_id"] == sid]
retrieved_amounts = [
item["amount"] for item in data if item["scenario_id"] == sid
]
for amount in amounts:
assert amount in retrieved_amounts
@@ -112,7 +116,8 @@ def test_multiple_opex_entries():
resp = client.get("/api/costs/opex")
assert resp.status_code == 200
data = resp.json()
retrieved_amounts = [item["amount"]
for item in data if item["scenario_id"] == sid]
retrieved_amounts = [
item["amount"] for item in data if item["scenario_id"] == sid
]
for amount in amounts:
assert amount in retrieved_amounts

View File

@@ -14,7 +14,13 @@ def _cleanup_currencies(db_session):
db_session.commit()
def _assert_currency(payload: Dict[str, object], code: str, name: str, symbol: str | None, is_active: bool) -> None:
def _assert_currency(
payload: Dict[str, object],
code: str,
name: str,
symbol: str | None,
is_active: bool,
) -> None:
assert payload["code"] == code
assert payload["name"] == name
assert payload["is_active"] is is_active
@@ -47,13 +53,21 @@ def test_create_currency_success(api_client, db_session):
def test_create_currency_conflict(api_client, db_session):
api_client.post(
"/api/currencies/",
json={"code": "CAD", "name": "Canadian Dollar",
"symbol": "$", "is_active": True},
json={
"code": "CAD",
"name": "Canadian Dollar",
"symbol": "$",
"is_active": True,
},
)
duplicate = api_client.post(
"/api/currencies/",
json={"code": "CAD", "name": "Canadian Dollar",
"symbol": "$", "is_active": True},
json={
"code": "CAD",
"name": "Canadian Dollar",
"symbol": "$",
"is_active": True,
},
)
assert duplicate.status_code == 409
@@ -61,8 +75,12 @@ def test_create_currency_conflict(api_client, db_session):
def test_update_currency_fields(api_client, db_session):
api_client.post(
"/api/currencies/",
json={"code": "GBP", "name": "British Pound",
"symbol": "£", "is_active": True},
json={
"code": "GBP",
"name": "British Pound",
"symbol": "£",
"is_active": True,
},
)
response = api_client.put(
@@ -77,8 +95,12 @@ def test_update_currency_fields(api_client, db_session):
def test_toggle_currency_activation(api_client, db_session):
api_client.post(
"/api/currencies/",
json={"code": "AUD", "name": "Australian Dollar",
"symbol": "A$", "is_active": True},
json={
"code": "AUD",
"name": "Australian Dollar",
"symbol": "A$",
"is_active": True,
},
)
response = api_client.patch(
@@ -97,5 +119,7 @@ def test_default_currency_cannot_be_deactivated(api_client, db_session):
json={"is_active": False},
)
assert response.status_code == 400
assert response.json()[
"detail"] == "The default currency cannot be deactivated."
assert (
response.json()["detail"]
== "The default currency cannot be deactivated."
)

View File

@@ -41,9 +41,10 @@ def test_create_capex_with_currency_code_and_list(api_client, seeded_currency):
resp = api_client.post("/api/costs/capex", json=payload)
assert resp.status_code == 200
data = resp.json()
assert data.get("currency_code") == seeded_currency.code or data.get(
"currency", {}
).get("code") == seeded_currency.code
assert (
data.get("currency_code") == seeded_currency.code
or data.get("currency", {}).get("code") == seeded_currency.code
)
def test_create_opex_with_currency_id(api_client, seeded_currency):

View File

@@ -30,7 +30,9 @@ def _create_scenario_and_equipment(client: TestClient):
return scenario_id, equipment_id
def _create_maintenance_payload(equipment_id: int, scenario_id: int, description: str):
def _create_maintenance_payload(
equipment_id: int, scenario_id: int, description: str
):
return {
"equipment_id": equipment_id,
"scenario_id": scenario_id,
@@ -43,7 +45,8 @@ def _create_maintenance_payload(equipment_id: int, scenario_id: int, description
def test_create_and_list_maintenance(client: TestClient):
scenario_id, equipment_id = _create_scenario_and_equipment(client)
payload = _create_maintenance_payload(
equipment_id, scenario_id, "Create maintenance")
equipment_id, scenario_id, "Create maintenance"
)
response = client.post("/api/maintenance/", json=payload)
assert response.status_code == 201
@@ -95,7 +98,8 @@ def test_update_maintenance(client: TestClient):
}
response = client.put(
f"/api/maintenance/{maintenance_id}", json=update_payload)
f"/api/maintenance/{maintenance_id}", json=update_payload
)
assert response.status_code == 200
updated = response.json()
assert updated["maintenance_date"] == "2025-11-01"
@@ -108,7 +112,8 @@ def test_delete_maintenance(client: TestClient):
create_response = client.post(
"/api/maintenance/",
json=_create_maintenance_payload(
equipment_id, scenario_id, "Delete maintenance"),
equipment_id, scenario_id, "Delete maintenance"
),
)
assert create_response.status_code == 201
maintenance_id = create_response.json()["id"]

View File

@@ -67,7 +67,10 @@ def test_create_and_list_parameter():
def test_create_parameter_for_missing_scenario():
payload: Dict[str, Any] = {
"scenario_id": 0, "name": "invalid", "value": 1.0}
"scenario_id": 0,
"name": "invalid",
"value": 1.0,
}
response = client.post("/api/parameters/", json=payload)
assert response.status_code == 404
assert response.json()["detail"] == "Scenario not found"

View File

@@ -42,7 +42,11 @@ def test_list_production_filters_by_scenario(client: TestClient) -> None:
target_scenario = _create_scenario(client)
other_scenario = _create_scenario(client)
for scenario_id, amount in [(target_scenario, 100.0), (target_scenario, 150.0), (other_scenario, 200.0)]:
for scenario_id, amount in [
(target_scenario, 100.0),
(target_scenario, 150.0),
(other_scenario, 200.0),
]:
response = client.post(
"/api/production/",
json={
@@ -57,8 +61,11 @@ def test_list_production_filters_by_scenario(client: TestClient) -> None:
list_response = client.get("/api/production/")
assert list_response.status_code == 200
items = [item for item in list_response.json()
if item["scenario_id"] == target_scenario]
items = [
item
for item in list_response.json()
if item["scenario_id"] == target_scenario
]
assert {item["amount"] for item in items} == {100.0, 150.0}

View File

@@ -50,9 +50,11 @@ def test_generate_report_with_values():
def test_generate_report_single_value():
report = generate_report([
{"iteration": 1, "result": 42.0},
])
report = generate_report(
[
{"iteration": 1, "result": 42.0},
]
)
assert report["count"] == 1
assert report["std_dev"] == 0.0
assert report["variance"] == 0.0
@@ -105,8 +107,10 @@ def test_reporting_endpoint_success(client: TestClient):
validation_error_cases: List[tuple[List[Any], str]] = [
(["not-a-dict"], "Entry at index 0 must be an object"),
([{"iteration": 1}], "Entry at index 0 must include numeric 'result'"),
([{"iteration": 1, "result": "bad"}],
"Entry at index 0 must include numeric 'result'"),
(
[{"iteration": 1, "result": "bad"}],
"Entry at index 0 must include numeric 'result'",
),
]

View File

@@ -27,7 +27,7 @@ def test_parameter_create_missing_scenario_returns_404(
@pytest.mark.usefixtures("invalid_request_payloads")
def test_parameter_create_invalid_distribution_is_422(
api_client: TestClient
api_client: TestClient,
) -> None:
response = api_client.post(
"/api/parameters/",
@@ -90,6 +90,5 @@ def test_maintenance_negative_cost_rejected_by_schema(
payload = invalid_request_payloads["maintenance_negative_cost"]
response = api_client.post("/api/maintenance/", json=payload)
assert response.status_code == 422
error_locations = [tuple(item["loc"])
for item in response.json()["detail"]]
error_locations = [tuple(item["loc"]) for item in response.json()["detail"]]
assert ("body", "cost") in error_locations

View File

@@ -42,7 +42,7 @@ def test_update_css_settings_persists_changes(
@pytest.mark.usefixtures("db_session")
def test_update_css_settings_invalid_value_returns_422(
api_client: TestClient
api_client: TestClient,
) -> None:
response = api_client.put(
"/api/settings/css",

View File

@@ -20,8 +20,14 @@ def fixture_clean_env(monkeypatch: pytest.MonkeyPatch) -> Dict[str, str]:
def test_css_key_to_env_var_formatting():
assert settings_service.css_key_to_env_var("--color-background") == "CALMINER_THEME_COLOR_BACKGROUND"
assert settings_service.css_key_to_env_var("--color-primary-stronger") == "CALMINER_THEME_COLOR_PRIMARY_STRONGER"
assert (
settings_service.css_key_to_env_var("--color-background")
== "CALMINER_THEME_COLOR_BACKGROUND"
)
assert (
settings_service.css_key_to_env_var("--color-primary-stronger")
== "CALMINER_THEME_COLOR_PRIMARY_STRONGER"
)
@pytest.mark.parametrize(
@@ -33,7 +39,9 @@ def test_css_key_to_env_var_formatting():
("--color-text-secondary", "hsla(210, 40%, 40%, 1)"),
],
)
def test_read_css_color_env_overrides_valid_values(clean_env, env_key, env_value):
def test_read_css_color_env_overrides_valid_values(
clean_env, env_key, env_value
):
env_var = settings_service.css_key_to_env_var(env_key)
clean_env[env_var] = env_value
@@ -50,7 +58,9 @@ def test_read_css_color_env_overrides_valid_values(clean_env, env_key, env_value
"rgb(1,2)", # malformed rgb
],
)
def test_read_css_color_env_overrides_invalid_values_raise(clean_env, invalid_value):
def test_read_css_color_env_overrides_invalid_values_raise(
clean_env, invalid_value
):
env_var = settings_service.css_key_to_env_var("--color-background")
clean_env[env_var] = invalid_value
@@ -64,7 +74,9 @@ def test_read_css_color_env_overrides_ignores_missing(clean_env):
def test_list_css_env_override_rows_returns_structured_data(clean_env):
clean_env[settings_service.css_key_to_env_var("--color-primary")] = "#123456"
clean_env[settings_service.css_key_to_env_var("--color-primary")] = (
"#123456"
)
rows = settings_service.list_css_env_override_rows(clean_env)
assert rows == [
{

View File

@@ -31,10 +31,13 @@ def setup_instance(mock_config: DatabaseConfig) -> DatabaseSetup:
return DatabaseSetup(mock_config, dry_run=True)
def test_seed_baseline_data_dry_run_skips_verification(setup_instance: DatabaseSetup) -> None:
with mock.patch("scripts.seed_data.run_with_namespace") as seed_run, mock.patch.object(
setup_instance, "_verify_seeded_data"
) as verify_mock:
def test_seed_baseline_data_dry_run_skips_verification(
setup_instance: DatabaseSetup,
) -> None:
with (
mock.patch("scripts.seed_data.run_with_namespace") as seed_run,
mock.patch.object(setup_instance, "_verify_seeded_data") as verify_mock,
):
setup_instance.seed_baseline_data(dry_run=True)
seed_run.assert_called_once()
@@ -47,13 +50,16 @@ def test_seed_baseline_data_dry_run_skips_verification(setup_instance: DatabaseS
verify_mock.assert_not_called()
def test_seed_baseline_data_invokes_verification(setup_instance: DatabaseSetup) -> None:
def test_seed_baseline_data_invokes_verification(
setup_instance: DatabaseSetup,
) -> None:
expected_currencies = {code for code, *_ in seed_data.CURRENCY_SEEDS}
expected_units = {code for code, *_ in seed_data.MEASUREMENT_UNIT_SEEDS}
with mock.patch("scripts.seed_data.run_with_namespace") as seed_run, mock.patch.object(
setup_instance, "_verify_seeded_data"
) as verify_mock:
with (
mock.patch("scripts.seed_data.run_with_namespace") as seed_run,
mock.patch.object(setup_instance, "_verify_seeded_data") as verify_mock,
):
setup_instance.seed_baseline_data(dry_run=False)
seed_run.assert_called_once()
@@ -67,7 +73,9 @@ def test_seed_baseline_data_invokes_verification(setup_instance: DatabaseSetup)
)
def test_run_migrations_applies_baseline_when_missing(mock_config: DatabaseConfig, tmp_path) -> None:
def test_run_migrations_applies_baseline_when_missing(
mock_config: DatabaseConfig, tmp_path
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
baseline = tmp_path / "000_base.sql"
@@ -88,15 +96,24 @@ def test_run_migrations_applies_baseline_when_missing(mock_config: DatabaseConfi
cursor_context.__enter__.return_value = cursor_mock
connection_mock.cursor.return_value = cursor_context
with mock.patch.object(
setup_instance, "_application_connection", return_value=connection_mock
), mock.patch.object(
setup_instance, "_migrations_table_exists", return_value=True
), mock.patch.object(
setup_instance, "_fetch_applied_migrations", return_value=set()
), mock.patch.object(
setup_instance, "_apply_migration_file", side_effect=capture_migration
) as apply_mock:
with (
mock.patch.object(
setup_instance,
"_application_connection",
return_value=connection_mock,
),
mock.patch.object(
setup_instance, "_migrations_table_exists", return_value=True
),
mock.patch.object(
setup_instance, "_fetch_applied_migrations", return_value=set()
),
mock.patch.object(
setup_instance,
"_apply_migration_file",
side_effect=capture_migration,
) as apply_mock,
):
setup_instance.run_migrations(tmp_path)
assert apply_mock.call_count == 1
@@ -121,17 +138,24 @@ def test_run_migrations_noop_when_all_files_already_applied(
connection_mock, cursor_mock = _connection_with_cursor()
with mock.patch.object(
setup_instance, "_application_connection", return_value=connection_mock
), mock.patch.object(
setup_instance, "_migrations_table_exists", return_value=True
), mock.patch.object(
setup_instance,
"_fetch_applied_migrations",
return_value={"000_base.sql", "20251022_add_other.sql"},
), mock.patch.object(
setup_instance, "_apply_migration_file"
) as apply_mock:
with (
mock.patch.object(
setup_instance,
"_application_connection",
return_value=connection_mock,
),
mock.patch.object(
setup_instance, "_migrations_table_exists", return_value=True
),
mock.patch.object(
setup_instance,
"_fetch_applied_migrations",
return_value={"000_base.sql", "20251022_add_other.sql"},
),
mock.patch.object(
setup_instance, "_apply_migration_file"
) as apply_mock,
):
setup_instance.run_migrations(tmp_path)
apply_mock.assert_not_called()
@@ -148,12 +172,16 @@ def _connection_with_cursor() -> tuple[mock.MagicMock, mock.MagicMock]:
return connection_mock, cursor_mock
def test_verify_seeded_data_raises_when_currency_missing(mock_config: DatabaseConfig) -> None:
def test_verify_seeded_data_raises_when_currency_missing(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
connection_mock, cursor_mock = _connection_with_cursor()
cursor_mock.fetchall.return_value = [("USD", True)]
with mock.patch.object(setup_instance, "_application_connection", return_value=connection_mock):
with mock.patch.object(
setup_instance, "_application_connection", return_value=connection_mock
):
with pytest.raises(RuntimeError) as exc:
setup_instance._verify_seeded_data(
expected_currency_codes={"USD", "EUR"},
@@ -163,12 +191,16 @@ def test_verify_seeded_data_raises_when_currency_missing(mock_config: DatabaseCo
assert "EUR" in str(exc.value)
def test_verify_seeded_data_raises_when_default_currency_inactive(mock_config: DatabaseConfig) -> None:
def test_verify_seeded_data_raises_when_default_currency_inactive(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
connection_mock, cursor_mock = _connection_with_cursor()
cursor_mock.fetchall.return_value = [("USD", False)]
with mock.patch.object(setup_instance, "_application_connection", return_value=connection_mock):
with mock.patch.object(
setup_instance, "_application_connection", return_value=connection_mock
):
with pytest.raises(RuntimeError) as exc:
setup_instance._verify_seeded_data(
expected_currency_codes={"USD"},
@@ -178,12 +210,16 @@ def test_verify_seeded_data_raises_when_default_currency_inactive(mock_config: D
assert "inactive" in str(exc.value)
def test_verify_seeded_data_raises_when_units_missing(mock_config: DatabaseConfig) -> None:
def test_verify_seeded_data_raises_when_units_missing(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
connection_mock, cursor_mock = _connection_with_cursor()
cursor_mock.fetchall.return_value = [("tonnes", True)]
with mock.patch.object(setup_instance, "_application_connection", return_value=connection_mock):
with mock.patch.object(
setup_instance, "_application_connection", return_value=connection_mock
):
with pytest.raises(RuntimeError) as exc:
setup_instance._verify_seeded_data(
expected_currency_codes=set(),
@@ -193,12 +229,18 @@ def test_verify_seeded_data_raises_when_units_missing(mock_config: DatabaseConfi
assert "liters" in str(exc.value)
def test_verify_seeded_data_raises_when_measurement_table_missing(mock_config: DatabaseConfig) -> None:
def test_verify_seeded_data_raises_when_measurement_table_missing(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
connection_mock, cursor_mock = _connection_with_cursor()
cursor_mock.execute.side_effect = psycopg_errors.UndefinedTable("relation does not exist")
cursor_mock.execute.side_effect = psycopg_errors.UndefinedTable(
"relation does not exist"
)
with mock.patch.object(setup_instance, "_application_connection", return_value=connection_mock):
with mock.patch.object(
setup_instance, "_application_connection", return_value=connection_mock
):
with pytest.raises(RuntimeError) as exc:
setup_instance._verify_seeded_data(
expected_currency_codes=set(),
@@ -226,9 +268,14 @@ def test_seed_baseline_data_rerun_uses_existing_records(
unit_rows,
]
with mock.patch.object(
setup_instance, "_application_connection", return_value=connection_mock
), mock.patch("scripts.seed_data.run_with_namespace") as seed_run:
with (
mock.patch.object(
setup_instance,
"_application_connection",
return_value=connection_mock,
),
mock.patch("scripts.seed_data.run_with_namespace") as seed_run,
):
setup_instance.seed_baseline_data(dry_run=False)
setup_instance.seed_baseline_data(dry_run=False)
@@ -240,7 +287,9 @@ def test_seed_baseline_data_rerun_uses_existing_records(
assert cursor_mock.execute.call_count == 4
def test_ensure_database_raises_with_context(mock_config: DatabaseConfig) -> None:
def test_ensure_database_raises_with_context(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
connection_mock = mock.MagicMock()
cursor_mock = mock.MagicMock()
@@ -248,14 +297,18 @@ def test_ensure_database_raises_with_context(mock_config: DatabaseConfig) -> Non
cursor_mock.execute.side_effect = [None, psycopg2.Error("create_fail")]
connection_mock.cursor.return_value = cursor_mock
with mock.patch.object(setup_instance, "_admin_connection", return_value=connection_mock):
with mock.patch.object(
setup_instance, "_admin_connection", return_value=connection_mock
):
with pytest.raises(RuntimeError) as exc:
setup_instance.ensure_database()
assert "Failed to create database" in str(exc.value)
def test_ensure_role_raises_with_context_during_creation(mock_config: DatabaseConfig) -> None:
def test_ensure_role_raises_with_context_during_creation(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
admin_conn, admin_cursor = _connection_with_cursor()
@@ -295,7 +348,9 @@ def test_ensure_role_raises_with_context_during_privilege_grants(
assert "Failed to grant privileges" in str(exc.value)
def test_ensure_database_dry_run_skips_creation(mock_config: DatabaseConfig) -> None:
def test_ensure_database_dry_run_skips_creation(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=True)
connection_mock = mock.MagicMock()
@@ -303,45 +358,59 @@ def test_ensure_database_dry_run_skips_creation(mock_config: DatabaseConfig) ->
cursor_mock.fetchone.return_value = None
connection_mock.cursor.return_value = cursor_mock
with mock.patch.object(setup_instance, "_admin_connection", return_value=connection_mock), mock.patch(
"scripts.setup_database.logger"
) as logger_mock:
with (
mock.patch.object(
setup_instance, "_admin_connection", return_value=connection_mock
),
mock.patch("scripts.setup_database.logger") as logger_mock,
):
setup_instance.ensure_database()
# expect only existence check, no create attempt
cursor_mock.execute.assert_called_once()
logger_mock.info.assert_any_call(
"Dry run: would create database '%s'. Run without --dry-run to proceed.", mock_config.database
"Dry run: would create database '%s'. Run without --dry-run to proceed.",
mock_config.database,
)
def test_ensure_role_dry_run_skips_creation_and_grants(mock_config: DatabaseConfig) -> None:
def test_ensure_role_dry_run_skips_creation_and_grants(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=True)
admin_conn, admin_cursor = _connection_with_cursor()
admin_cursor.fetchone.return_value = None
with mock.patch.object(
setup_instance,
"_admin_connection",
side_effect=[admin_conn],
) as conn_mock, mock.patch("scripts.setup_database.logger") as logger_mock:
with (
mock.patch.object(
setup_instance,
"_admin_connection",
side_effect=[admin_conn],
) as conn_mock,
mock.patch("scripts.setup_database.logger") as logger_mock,
):
setup_instance.ensure_role()
assert conn_mock.call_count == 1
admin_cursor.execute.assert_called_once()
logger_mock.info.assert_any_call(
"Dry run: would create role '%s'. Run without --dry-run to apply.", mock_config.user
"Dry run: would create role '%s'. Run without --dry-run to apply.",
mock_config.user,
)
def test_register_rollback_skipped_when_dry_run(mock_config: DatabaseConfig) -> None:
def test_register_rollback_skipped_when_dry_run(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=True)
setup_instance._register_rollback("noop", lambda: None)
assert setup_instance._rollback_actions == []
def test_execute_rollbacks_runs_in_reverse_order(mock_config: DatabaseConfig) -> None:
def test_execute_rollbacks_runs_in_reverse_order(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
calls: list[str] = []
@@ -362,16 +431,24 @@ def test_execute_rollbacks_runs_in_reverse_order(mock_config: DatabaseConfig) ->
assert setup_instance._rollback_actions == []
def test_ensure_database_registers_rollback_action(mock_config: DatabaseConfig) -> None:
def test_ensure_database_registers_rollback_action(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
connection_mock = mock.MagicMock()
cursor_mock = mock.MagicMock()
cursor_mock.fetchone.return_value = None
connection_mock.cursor.return_value = cursor_mock
with mock.patch.object(setup_instance, "_admin_connection", return_value=connection_mock), mock.patch.object(
setup_instance, "_register_rollback"
) as register_mock, mock.patch.object(setup_instance, "_drop_database") as drop_mock:
with (
mock.patch.object(
setup_instance, "_admin_connection", return_value=connection_mock
),
mock.patch.object(
setup_instance, "_register_rollback"
) as register_mock,
mock.patch.object(setup_instance, "_drop_database") as drop_mock,
):
setup_instance.ensure_database()
register_mock.assert_called_once()
label, action = register_mock.call_args[0]
@@ -380,24 +457,29 @@ def test_ensure_database_registers_rollback_action(mock_config: DatabaseConfig)
drop_mock.assert_called_once_with(mock_config.database)
def test_ensure_role_registers_rollback_actions(mock_config: DatabaseConfig) -> None:
def test_ensure_role_registers_rollback_actions(
mock_config: DatabaseConfig,
) -> None:
setup_instance = DatabaseSetup(mock_config, dry_run=False)
admin_conn, admin_cursor = _connection_with_cursor()
admin_cursor.fetchone.return_value = None
privilege_conn, privilege_cursor = _connection_with_cursor()
with mock.patch.object(
setup_instance,
"_admin_connection",
side_effect=[admin_conn, privilege_conn],
), mock.patch.object(
setup_instance, "_register_rollback"
) as register_mock, mock.patch.object(
setup_instance, "_drop_role"
) as drop_mock, mock.patch.object(
setup_instance, "_revoke_role_privileges"
) as revoke_mock:
with (
mock.patch.object(
setup_instance,
"_admin_connection",
side_effect=[admin_conn, privilege_conn],
),
mock.patch.object(
setup_instance, "_register_rollback"
) as register_mock,
mock.patch.object(setup_instance, "_drop_role") as drop_mock,
mock.patch.object(
setup_instance, "_revoke_role_privileges"
) as revoke_mock,
):
setup_instance.ensure_role()
assert register_mock.call_count == 2
drop_label, drop_action = register_mock.call_args_list[0][0]
@@ -413,7 +495,9 @@ def test_ensure_role_registers_rollback_actions(mock_config: DatabaseConfig) ->
revoke_mock.assert_called_once()
def test_main_triggers_rollbacks_on_failure(mock_config: DatabaseConfig) -> None:
def test_main_triggers_rollbacks_on_failure(
mock_config: DatabaseConfig,
) -> None:
args = argparse.Namespace(
ensure_database=True,
ensure_role=True,
@@ -437,11 +521,13 @@ def test_main_triggers_rollbacks_on_failure(mock_config: DatabaseConfig) -> None
verbose=0,
)
with mock.patch.object(setup_db_module, "parse_args", return_value=args), mock.patch.object(
setup_db_module.DatabaseConfig, "from_env", return_value=mock_config
), mock.patch.object(
setup_db_module, "DatabaseSetup"
) as setup_cls:
with (
mock.patch.object(setup_db_module, "parse_args", return_value=args),
mock.patch.object(
setup_db_module.DatabaseConfig, "from_env", return_value=mock_config
),
mock.patch.object(setup_db_module, "DatabaseSetup") as setup_cls,
):
setup_instance = mock.MagicMock()
setup_instance.dry_run = False
setup_instance._rollback_actions = [

View File

@@ -19,7 +19,12 @@ def client(api_client: TestClient) -> TestClient:
def test_run_simulation_function_generates_samples():
params: List[Dict[str, Any]] = [
{"name": "grade", "value": 1.8, "distribution": "normal", "std_dev": 0.2},
{
"name": "grade",
"value": 1.8,
"distribution": "normal",
"std_dev": 0.2,
},
{
"name": "recovery",
"value": 0.9,
@@ -45,7 +50,10 @@ def test_run_simulation_with_zero_iterations_returns_empty():
@pytest.mark.parametrize(
"parameter_payload,error_message",
[
({"name": "missing-value"}, "Parameter at index 0 must include 'value'"),
(
{"name": "missing-value"},
"Parameter at index 0 must include 'value'",
),
(
{
"name": "bad-dist",
@@ -110,7 +118,8 @@ def test_run_simulation_triangular_sampling_path():
span = 10.0 * DEFAULT_UNIFORM_SPAN_RATIO
rng = Random(seed)
expected_samples = [
rng.triangular(10.0 - span, 10.0 + span, 10.0) for _ in range(iterations)
rng.triangular(10.0 - span, 10.0 + span, 10.0)
for _ in range(iterations)
]
actual_samples = [entry["result"] for entry in results]
for actual, expected in zip(actual_samples, expected_samples):
@@ -156,9 +165,7 @@ def test_simulation_endpoint_no_params(client: TestClient):
assert resp.json()["detail"] == "No parameters provided"
def test_simulation_endpoint_success(
client: TestClient, db_session: Session
):
def test_simulation_endpoint_success(client: TestClient, db_session: Session):
scenario_payload: Dict[str, Any] = {
"name": f"SimScenario-{uuid4()}",
"description": "Simulation test",
@@ -168,7 +175,12 @@ def test_simulation_endpoint_success(
scenario_id = scenario_resp.json()["id"]
params: List[Dict[str, Any]] = [
{"name": "param1", "value": 2.5, "distribution": "normal", "std_dev": 0.5}
{
"name": "param1",
"value": 2.5,
"distribution": "normal",
"std_dev": 0.5,
}
]
payload: Dict[str, Any] = {
"scenario_id": scenario_id,

View File

@@ -0,0 +1,63 @@
import pytest
from sqlalchemy.orm import Session
from fastapi.testclient import TestClient
from main import app
from models.theme_setting import ThemeSetting
from services.settings import save_theme_settings, get_theme_settings
client = TestClient(app)
def test_save_theme_settings(db_session: Session):
theme_data = {
"theme_name": "dark",
"primary_color": "#000000",
"secondary_color": "#333333",
"accent_color": "#ff0000",
"background_color": "#1a1a1a",
"text_color": "#ffffff"
}
saved_setting = save_theme_settings(db_session, theme_data)
assert str(saved_setting.theme_name) == "dark"
assert str(saved_setting.primary_color) == "#000000"
def test_get_theme_settings(db_session: Session):
# Create a theme setting first
theme_data = {
"theme_name": "light",
"primary_color": "#ffffff",
"secondary_color": "#cccccc",
"accent_color": "#0000ff",
"background_color": "#f0f0f0",
"text_color": "#000000"
}
save_theme_settings(db_session, theme_data)
settings = get_theme_settings(db_session)
assert settings["theme_name"] == "light"
assert settings["primary_color"] == "#ffffff"
def test_theme_settings_api(api_client):
# Test API endpoint for saving theme settings
theme_data = {
"theme_name": "test_theme",
"primary_color": "#123456",
"secondary_color": "#789abc",
"accent_color": "#def012",
"background_color": "#345678",
"text_color": "#9abcde"
}
response = api_client.post("/api/settings/theme", json=theme_data)
assert response.status_code == 200
assert response.json()["theme"]["theme_name"] == "test_theme"
# Test API endpoint for getting theme settings
response = api_client.get("/api/settings/theme")
assert response.status_code == 200
assert response.json()["theme_name"] == "test_theme"

View File

@@ -21,11 +21,18 @@ def test_dashboard_route_provides_summary(
assert context.get("report_available") is True
metric_labels = {item["label"] for item in context["summary_metrics"]}
assert {"CAPEX Total", "OPEX Total", "Production", "Simulation Iterations"}.issubset(metric_labels)
assert {
"CAPEX Total",
"OPEX Total",
"Production",
"Simulation Iterations",
}.issubset(metric_labels)
scenario = cast(Scenario, seeded_ui_data["scenario"])
scenario_row = next(
row for row in context["scenario_rows"] if row["scenario_name"] == scenario.name
row
for row in context["scenario_rows"]
if row["scenario_name"] == scenario.name
)
assert scenario_row["iterations"] == 3
assert scenario_row["simulation_mean_display"] == "971,666.67"
@@ -81,7 +88,9 @@ def test_dashboard_data_endpoint_returns_aggregates(
payload = response.json()
assert payload["report_available"] is True
metric_map = {item["label"]: item["value"] for item in payload["summary_metrics"]}
metric_map = {
item["label"]: item["value"] for item in payload["summary_metrics"]
}
assert metric_map["CAPEX Total"].startswith("$")
assert metric_map["Maintenance Cost"].startswith("$")
@@ -99,7 +108,9 @@ def test_dashboard_data_endpoint_returns_aggregates(
activity_labels = payload["scenario_activity_chart"]["labels"]
activity_idx = activity_labels.index(scenario.name)
assert payload["scenario_activity_chart"]["production"][activity_idx] == 800.0
assert (
payload["scenario_activity_chart"]["production"][activity_idx] == 800.0
)
@pytest.mark.parametrize(
@@ -154,7 +165,10 @@ def test_settings_route_provides_css_context(
assert "css_env_override_meta" in context
assert context["css_variables"]["--color-accent"] == "#abcdef"
assert context["css_defaults"]["--color-accent"] == settings_service.CSS_COLOR_DEFAULTS["--color-accent"]
assert (
context["css_defaults"]["--color-accent"]
== settings_service.CSS_COLOR_DEFAULTS["--color-accent"]
)
assert context["css_env_overrides"]["--color-accent"] == "#abcdef"
override_rows = context["css_env_override_rows"]