167 lines
4.7 KiB
Python
167 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import date, datetime, timezone
|
|
from decimal import Decimal
|
|
from typing import Any, Iterable
|
|
|
|
import pytest
|
|
|
|
from services.export_serializers import (
|
|
CSVExportColumn,
|
|
CSVExporter,
|
|
default_formatter,
|
|
default_project_columns,
|
|
default_scenario_columns,
|
|
format_date_iso,
|
|
format_datetime_utc,
|
|
format_decimal,
|
|
stream_projects_to_csv,
|
|
stream_scenarios_to_csv,
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class DummyProject:
|
|
name: str
|
|
location: str | None = None
|
|
operation_type: str = "open_pit"
|
|
description: str | None = None
|
|
created_at: datetime | None = None
|
|
updated_at: datetime | None = None
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class DummyScenario:
|
|
project: DummyProject | None
|
|
name: str
|
|
status: str = "draft"
|
|
start_date: date | None = None
|
|
end_date: date | None = None
|
|
discount_rate: Decimal | None = None
|
|
currency: str | None = None
|
|
primary_resource: str | None = None
|
|
description: str | None = None
|
|
created_at: datetime | None = None
|
|
updated_at: datetime | None = None
|
|
|
|
|
|
def collect_csv_bytes(chunks: Iterable[bytes]) -> list[str]:
|
|
return [chunk.decode("utf-8") for chunk in chunks]
|
|
|
|
|
|
def test_csv_exporter_writes_header_and_rows() -> None:
|
|
exporter = CSVExporter(
|
|
[
|
|
CSVExportColumn("Name", "name"),
|
|
CSVExportColumn("Location", "location"),
|
|
]
|
|
)
|
|
|
|
project = DummyProject(name="Alpha", location="Nevada")
|
|
|
|
chunks = collect_csv_bytes(exporter.iter_bytes([project]))
|
|
|
|
assert chunks[0] == "Name,Location\n"
|
|
assert chunks[1] == "Alpha,Nevada\n"
|
|
|
|
|
|
def test_csv_exporter_handles_optional_values_and_default_formatter() -> None:
|
|
exporter = CSVExporter(
|
|
[
|
|
CSVExportColumn("Name", "name"),
|
|
CSVExportColumn("Description", "description"),
|
|
]
|
|
)
|
|
|
|
project = DummyProject(name="Bravo")
|
|
|
|
chunks = collect_csv_bytes(exporter.iter_bytes([project]))
|
|
|
|
assert chunks[-1] == "Bravo,\n"
|
|
|
|
|
|
def test_stream_projects_uses_default_columns() -> None:
|
|
projects = [
|
|
DummyProject(
|
|
name="Alpha",
|
|
location="Nevada",
|
|
operation_type="open_pit",
|
|
description="Primary",
|
|
created_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
|
|
updated_at=datetime(2025, 1, 2, tzinfo=timezone.utc),
|
|
)
|
|
]
|
|
|
|
chunks = collect_csv_bytes(stream_projects_to_csv(projects))
|
|
|
|
assert chunks[0].startswith("name,location,operation_type")
|
|
assert any("Alpha" in chunk for chunk in chunks)
|
|
|
|
|
|
def test_stream_scenarios_resolves_project_name_accessor() -> None:
|
|
project = DummyProject(name="Project X")
|
|
scenario = DummyScenario(project=project, name="Scenario A")
|
|
|
|
chunks = collect_csv_bytes(stream_scenarios_to_csv([scenario]))
|
|
|
|
assert "Project X" in chunks[-1]
|
|
assert "Scenario A" in chunks[-1]
|
|
|
|
|
|
def test_custom_formatter_applies() -> None:
|
|
def uppercase(value: Any) -> str:
|
|
return str(value).upper() if value is not None else ""
|
|
|
|
exporter = CSVExporter([
|
|
CSVExportColumn("Name", "name", formatter=uppercase),
|
|
])
|
|
|
|
chunks = collect_csv_bytes(
|
|
exporter.iter_bytes([DummyProject(name="alpha")]))
|
|
|
|
assert chunks[-1] == "ALPHA\n"
|
|
|
|
|
|
def test_default_formatter_handles_multiple_types() -> None:
|
|
assert default_formatter(None) == ""
|
|
assert default_formatter(True) == "true"
|
|
assert default_formatter(False) == "false"
|
|
assert default_formatter(Decimal("1.234")) == "1.23"
|
|
assert default_formatter(
|
|
datetime(2025, 1, 1, tzinfo=timezone.utc)).endswith("Z")
|
|
assert default_formatter(date(2025, 1, 1)) == "2025-01-01"
|
|
|
|
|
|
def test_format_helpers() -> None:
|
|
assert format_date_iso(date(2025, 5, 1)) == "2025-05-01"
|
|
assert format_date_iso("not-a-date") == ""
|
|
|
|
ts = datetime(2025, 5, 1, 12, 0, tzinfo=timezone.utc)
|
|
assert format_datetime_utc(ts) == "2025-05-01T12:00:00Z"
|
|
|
|
assert format_datetime_utc("nope") == ""
|
|
assert format_decimal(None) == ""
|
|
assert format_decimal(Decimal("12.345")) == "12.35"
|
|
assert format_decimal(10) == "10.00"
|
|
|
|
|
|
def test_default_project_columns_includes_required_fields() -> None:
|
|
columns = default_project_columns()
|
|
headers = [column.header for column in columns]
|
|
assert headers[:3] == ["name", "location", "operation_type"]
|
|
|
|
|
|
def test_default_scenario_columns_handles_missing_project() -> None:
|
|
scenario = DummyScenario(project=None, name="Orphan Scenario")
|
|
|
|
exporter = CSVExporter(default_scenario_columns())
|
|
chunks = collect_csv_bytes(exporter.iter_bytes([scenario]))
|
|
|
|
assert chunks[-1].startswith(",Orphan Scenario")
|
|
|
|
|
|
def test_csv_exporter_requires_columns() -> None:
|
|
with pytest.raises(ValueError):
|
|
CSVExporter([])
|