- Added monitoring metrics for project creation success and error handling in `ProjectRepository`. - Implemented similar monitoring for scenario creation in `ScenarioRepository`. - Refactored `run_monte_carlo` function in `simulation.py` to include timing and success/error metrics. - Introduced new CSS styles for headers, alerts, and navigation buttons in `main.css` and `projects.css`. - Created a new JavaScript file for navigation logic to handle chevron buttons. - Updated HTML templates to include new navigation buttons and improved styling for buttons. - Added tests for reporting service and routes to ensure proper functionality and access control. - Removed unused imports and optimized existing test files for better clarity and performance.
161 lines
4.6 KiB
Python
161 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
from io import BytesIO
|
|
from textwrap import dedent
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from services.importers import ImportResult, load_project_imports, load_scenario_imports
|
|
from schemas.imports import ProjectImportRow, ScenarioImportRow
|
|
|
|
|
|
def test_load_project_imports_from_csv() -> None:
|
|
csv_content = (
|
|
"name,location,operation_type,description\n"
|
|
"Project A,Chile,open pit,First project\n"
|
|
"Project B,,underground,Second project\n"
|
|
)
|
|
stream = BytesIO(csv_content.encode("utf-8"))
|
|
|
|
result = load_project_imports(stream, "projects.csv")
|
|
|
|
assert isinstance(result, ImportResult)
|
|
assert len(result.rows) == 2
|
|
assert not result.errors
|
|
first = result.rows[0]
|
|
assert first.row_number == 2
|
|
assert isinstance(first.data, ProjectImportRow)
|
|
assert first.data.name == "Project A"
|
|
assert first.data.operation_type.value == "open_pit"
|
|
second = result.rows[1]
|
|
assert second.row_number == 3
|
|
assert isinstance(second.data, ProjectImportRow)
|
|
assert second.data.location is None
|
|
|
|
|
|
def test_load_scenario_imports_from_excel() -> None:
|
|
df = pd.DataFrame(
|
|
[
|
|
{
|
|
"project_name": "Project A",
|
|
"name": "Scenario 1",
|
|
"status": "Active",
|
|
"start_date": "2025-01-01",
|
|
"end_date": "2025-12-31",
|
|
"discount_rate": "7.5%",
|
|
"currency": "usd",
|
|
"primary_resource": "Electricity",
|
|
}
|
|
]
|
|
)
|
|
buffer = BytesIO()
|
|
df.to_excel(buffer, index=False)
|
|
buffer.seek(0)
|
|
|
|
result = load_scenario_imports(buffer, "scenarios.xlsx")
|
|
|
|
assert len(result.rows) == 1
|
|
assert not result.errors
|
|
row = result.rows[0]
|
|
assert row.row_number == 2
|
|
assert isinstance(row.data, ScenarioImportRow)
|
|
assert row.data.status.value == "active"
|
|
assert row.data.currency == "USD"
|
|
assert row.data.discount_rate == pytest.approx(7.5)
|
|
|
|
|
|
def test_import_errors_include_row_numbers() -> None:
|
|
csv_content = "name,operation_type\n,open pit\n"
|
|
stream = BytesIO(csv_content.encode("utf-8"))
|
|
|
|
result = load_project_imports(stream, "projects.csv")
|
|
|
|
assert len(result.rows) == 0
|
|
assert len(result.errors) == 1
|
|
error = result.errors[0]
|
|
assert error.row_number == 2
|
|
assert error.field == "name"
|
|
assert "required" in error.message
|
|
|
|
|
|
def test_project_import_handles_missing_columns() -> None:
|
|
csv_content = "name\nProject Only\n"
|
|
stream = BytesIO(csv_content.encode("utf-8"))
|
|
|
|
result = load_project_imports(stream, "projects.csv")
|
|
|
|
assert result.rows == []
|
|
assert len(result.errors) == 1
|
|
error = result.errors[0]
|
|
assert error.row_number == 2
|
|
assert error.field == "operation_type"
|
|
|
|
|
|
def test_project_import_rejects_invalid_operation_type() -> None:
|
|
csv_content = "name,operation_type\nProject X,unknown\n"
|
|
stream = BytesIO(csv_content.encode("utf-8"))
|
|
|
|
result = load_project_imports(stream, "projects.csv")
|
|
|
|
assert len(result.rows) == 0
|
|
assert len(result.errors) == 1
|
|
error = result.errors[0]
|
|
assert error.row_number == 2
|
|
assert error.field == "operation_type"
|
|
|
|
|
|
def test_scenario_import_flags_invalid_dates() -> None:
|
|
csv_content = dedent(
|
|
"""
|
|
project_name,name,status,start_date,end_date
|
|
Project A,Scenario Reverse,Draft,2025-12-31,2025-01-01
|
|
"""
|
|
).strip()
|
|
stream = BytesIO(csv_content.encode("utf-8"))
|
|
|
|
result = load_scenario_imports(stream, "scenarios.csv")
|
|
|
|
assert len(result.rows) == 0
|
|
assert len(result.errors) == 1
|
|
error = result.errors[0]
|
|
assert error.row_number == 2
|
|
assert error.field is None
|
|
|
|
|
|
def test_scenario_import_handles_large_dataset() -> None:
|
|
buffer = BytesIO()
|
|
df = pd.DataFrame(
|
|
{
|
|
"project_name": ["Project"] * 500,
|
|
"name": [f"Scenario {i}" for i in range(500)],
|
|
"status": ["draft"] * 500,
|
|
}
|
|
)
|
|
df.to_csv(buffer, index=False)
|
|
buffer.seek(0)
|
|
|
|
result = load_scenario_imports(buffer, "bulk.csv")
|
|
|
|
assert len(result.rows) == 500
|
|
assert len(result.rows) == 500
|
|
|
|
|
|
def test_scenario_import_rejects_invalid_currency() -> None:
|
|
csv_content = dedent(
|
|
"""
|
|
project_name,name,currency
|
|
Project A,Scenario Invalid,US
|
|
"""
|
|
).strip()
|
|
stream = BytesIO(csv_content.encode("utf-8"))
|
|
|
|
result = load_scenario_imports(stream, "scenarios.csv")
|
|
|
|
assert not result.rows
|
|
assert result.errors
|
|
error = result.errors[0]
|
|
assert error.row_number == 2
|
|
assert error.field == "currency"
|
|
assert "Invalid currency code" in error.message
|