diff --git a/changelog.md b/changelog.md index e449756..1cbf0cb 100644 --- a/changelog.md +++ b/changelog.md @@ -32,3 +32,4 @@ - Completed the Authentication & RBAC checklist by shipping the new models, migrations, repositories, guard dependencies, and integration tests. - Documented the project/scenario import/export field mapping and file format guidelines in `calminer-docs/requirements/FR-008.md`, and introduced `schemas/imports.py` with Pydantic models that normalise incoming CSV/Excel rows for projects and scenarios. - Added `services/importers.py` to load CSV/XLSX files into the new import schemas, pulled in `openpyxl` for Excel support, and covered the parsing behaviour with `tests/test_import_parsing.py`. +- Expanded the import ingestion workflow with staging previews, transactional persistence commits, FastAPI preview/commit endpoints under `/imports`, and new API tests (`tests/test_import_ingestion.py`, `tests/test_import_api.py`) ensuring end-to-end coverage. diff --git a/tests/conftest.py b/tests/conftest.py index 10f8135..1b96454 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,12 +11,14 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool from config.database import Base -from dependencies import get_auth_session, get_unit_of_work +from dependencies import get_auth_session, get_import_ingestion_service, get_unit_of_work from models import User from routes.auth import router as auth_router 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 routes.imports import router as imports_router +from services.importers import ImportIngestionService from services.unit_of_work import UnitOfWork from services.session import AuthSession, SessionTokens @@ -51,6 +53,7 @@ def app(session_factory: sessionmaker) -> FastAPI: application.include_router(dashboard_router) application.include_router(projects_router) application.include_router(scenarios_router) + application.include_router(imports_router) def _override_uow() -> Iterator[UnitOfWork]: with UnitOfWork(session_factory=session_factory) as uow: @@ -58,6 +61,18 @@ def app(session_factory: sessionmaker) -> FastAPI: application.dependency_overrides[get_unit_of_work] = _override_uow + def _ingestion_uow_factory() -> UnitOfWork: + return UnitOfWork(session_factory=session_factory) + + ingestion_service = ImportIngestionService(_ingestion_uow_factory) + + def _override_ingestion_service() -> ImportIngestionService: + return ingestion_service + + application.dependency_overrides[ + get_import_ingestion_service + ] = _override_ingestion_service + with UnitOfWork(session_factory=session_factory) as uow: assert uow.users is not None uow.ensure_default_roles() diff --git a/tests/test_import_api.py b/tests/test_import_api.py new file mode 100644 index 0000000..47ae9d9 --- /dev/null +++ b/tests/test_import_api.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from fastapi.testclient import TestClient + +from models.project import MiningOperationType, Project +from models.scenario import Scenario, ScenarioStatus + + +def test_project_import_preview_and_commit_flow( + client: TestClient, + unit_of_work_factory, +) -> None: + with unit_of_work_factory() as uow: + assert uow.projects is not None + existing = Project( + name="Existing Project", + location="Chile", + operation_type=MiningOperationType.OPEN_PIT, + ) + uow.projects.create(existing) + + csv_content = ( + "name,location,operation_type\n" + "Existing Project,Peru,underground\n" + "New Project,Canada,open pit\n" + ) + + preview_response = client.post( + "/imports/projects/preview", + files={"file": ("projects.csv", csv_content, "text/csv")}, + ) + assert preview_response.status_code == 200 + preview_data = preview_response.json() + assert preview_data["summary"]["accepted"] == 2 + token = preview_data["stage_token"] + assert token + + commit_response = client.post( + "/imports/projects/commit", + json={"token": token}, + ) + assert commit_response.status_code == 200 + commit_data = commit_response.json() + assert commit_data["summary"] == {"created": 1, "updated": 1} + + with unit_of_work_factory() as uow: + assert uow.projects is not None + projects = {project.name: project for project in uow.projects.list()} + assert "Existing Project" in projects and "New Project" in projects + assert ( + projects["Existing Project"].operation_type + == MiningOperationType.UNDERGROUND + ) + + repeat_commit = client.post( + "/imports/projects/commit", + json={"token": token}, + ) + assert repeat_commit.status_code == 404 + + +def test_scenario_import_commit_invalid_token_returns_404( + client: TestClient, +) -> None: + response = client.post( + "/imports/scenarios/commit", + json={"token": "missing-token"}, + ) + assert response.status_code == 404 + assert "Unknown scenario import token" in response.json()["detail"]