feat: expand import ingestion workflow with staging previews, transactional commits, and new API tests
This commit is contained in:
@@ -32,3 +32,4 @@
|
|||||||
- Completed the Authentication & RBAC checklist by shipping the new models, migrations, repositories, guard dependencies, and integration tests.
|
- 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.
|
- 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`.
|
- 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.
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ from sqlalchemy.orm import sessionmaker
|
|||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from config.database import Base
|
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 models import User
|
||||||
from routes.auth import router as auth_router
|
from routes.auth import router as auth_router
|
||||||
from routes.dashboard import router as dashboard_router
|
from routes.dashboard import router as dashboard_router
|
||||||
from routes.projects import router as projects_router
|
from routes.projects import router as projects_router
|
||||||
from routes.scenarios import router as scenarios_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.unit_of_work import UnitOfWork
|
||||||
from services.session import AuthSession, SessionTokens
|
from services.session import AuthSession, SessionTokens
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ def app(session_factory: sessionmaker) -> FastAPI:
|
|||||||
application.include_router(dashboard_router)
|
application.include_router(dashboard_router)
|
||||||
application.include_router(projects_router)
|
application.include_router(projects_router)
|
||||||
application.include_router(scenarios_router)
|
application.include_router(scenarios_router)
|
||||||
|
application.include_router(imports_router)
|
||||||
|
|
||||||
def _override_uow() -> Iterator[UnitOfWork]:
|
def _override_uow() -> Iterator[UnitOfWork]:
|
||||||
with UnitOfWork(session_factory=session_factory) as uow:
|
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
|
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:
|
with UnitOfWork(session_factory=session_factory) as uow:
|
||||||
assert uow.users is not None
|
assert uow.users is not None
|
||||||
uow.ensure_default_roles()
|
uow.ensure_default_roles()
|
||||||
|
|||||||
70
tests/test_import_api.py
Normal file
70
tests/test_import_api.py
Normal file
@@ -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"]
|
||||||
Reference in New Issue
Block a user