mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Resolves the authenticated requester's email (ADR-0059), resolves + caps the property selection, pins the recipe (landlord_property_ids, recipient_email, package_name) onto one pre-created sub_task (raw SQL, dodging the mirror double-registration), and enqueues one message to the worker. Refuses double-submits (409) and oversized selections (400). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
"""Tests for the Bulk Document Download trigger endpoint (ADR-0060).
|
|
|
|
Session, SQS and requester-email seams are dependency-injected; tests run
|
|
against the ephemeral Postgres and record message bodies instead of calling AWS.
|
|
"""
|
|
|
|
import json
|
|
from collections.abc import Iterator
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import Engine
|
|
from sqlmodel import Session
|
|
|
|
from backend.app.dependencies import validate_token
|
|
from backend.app.documents import router as documents_router
|
|
from backend.app.documents.router import (
|
|
get_message_sender,
|
|
get_requesting_user_email,
|
|
get_session,
|
|
)
|
|
from domain.tasks.tasks import Task
|
|
from infrastructure.postgres.property_table import PropertyRow
|
|
from repositories.tasks.subtask_postgres_repository import SubTaskPostgresRepository
|
|
from repositories.tasks.task_postgres_repository import TaskPostgresRepository
|
|
|
|
PORTFOLIO_ID = 814
|
|
RECIPIENT = "tester@example.com"
|
|
|
|
|
|
@dataclass
|
|
class Api:
|
|
client: TestClient
|
|
engine: Engine
|
|
sent_bodies: list[str] = field(default_factory=list)
|
|
|
|
def seed_task(self) -> Task:
|
|
with Session(self.engine) as session:
|
|
return TaskPostgresRepository(session).create(
|
|
Task.create(task_source="app:bulk_document_download")
|
|
)
|
|
|
|
def seed_property(self, landlord_property_id: str) -> int:
|
|
with Session(self.engine) as session:
|
|
row = PropertyRow(
|
|
portfolio_id=PORTFOLIO_ID, landlord_property_id=landlord_property_id
|
|
)
|
|
session.add(row)
|
|
session.commit()
|
|
session.refresh(row)
|
|
assert row.id is not None
|
|
return row.id
|
|
|
|
def subtask_inputs(self, task: Task) -> list[dict[str, Any]]:
|
|
with Session(self.engine) as session:
|
|
subtasks = SubTaskPostgresRepository(session).list_by_task(task.id)
|
|
return [st.inputs or {} for st in subtasks]
|
|
|
|
|
|
@pytest.fixture
|
|
def api(db_engine: Engine) -> Api:
|
|
app = FastAPI()
|
|
app.include_router(documents_router.router, prefix="/v1")
|
|
harness = Api(client=TestClient(app), engine=db_engine)
|
|
|
|
def _session() -> Iterator[Session]:
|
|
with Session(db_engine) as session:
|
|
yield session
|
|
|
|
app.dependency_overrides[get_session] = _session
|
|
app.dependency_overrides[get_message_sender] = lambda: harness.sent_bodies.append
|
|
app.dependency_overrides[get_requesting_user_email] = lambda: RECIPIENT
|
|
app.dependency_overrides[validate_token] = lambda: "test-token"
|
|
return harness
|
|
|
|
|
|
def _trigger(api: Api, task: Task, property_ids: list[int]) -> Any:
|
|
return api.client.post(
|
|
"/v1/documents/bulk-download",
|
|
json={
|
|
"task_id": str(task.id),
|
|
"portfolio_id": PORTFOLIO_ID,
|
|
"property_ids": property_ids,
|
|
},
|
|
)
|
|
|
|
|
|
def test_pins_the_recipe_and_enqueues_one_message(api: Api) -> None:
|
|
# arrange — two selected properties, matched by landlord_property_id.
|
|
task = api.seed_task()
|
|
p1 = api.seed_property("LP1")
|
|
p2 = api.seed_property("LP2")
|
|
|
|
# act
|
|
response = _trigger(api, task, [p1, p2])
|
|
|
|
# assert — accepted; one sub_task pins the recipe; one message enqueued.
|
|
assert response.status_code == 202
|
|
inputs = api.subtask_inputs(task)
|
|
assert len(inputs) == 1
|
|
assert inputs[0]["landlord_property_ids"] == ["LP1", "LP2"]
|
|
assert inputs[0]["recipient_email"] == RECIPIENT
|
|
assert len(api.sent_bodies) == 1
|
|
message = json.loads(api.sent_bodies[0])
|
|
assert message["task_id"] == str(task.id)
|
|
assert "subtask_id" in message
|
|
|
|
|
|
def test_rejects_a_selection_over_the_cap(api: Api, monkeypatch: Any) -> None:
|
|
# arrange — cap of 1, two selected properties.
|
|
monkeypatch.setattr(documents_router, "MAX_PROPERTIES", 1)
|
|
task = api.seed_task()
|
|
p1 = api.seed_property("LP1")
|
|
p2 = api.seed_property("LP2")
|
|
|
|
# act
|
|
response = _trigger(api, task, [p1, p2])
|
|
|
|
# assert — refused; nothing created or sent.
|
|
assert response.status_code == 400
|
|
assert api.subtask_inputs(task) == []
|
|
assert api.sent_bodies == []
|
|
|
|
|
|
def test_rejects_a_task_that_already_has_a_subtask(api: Api) -> None:
|
|
# arrange — a task already triggered once.
|
|
task = api.seed_task()
|
|
p1 = api.seed_property("LP1")
|
|
assert _trigger(api, task, [p1]).status_code == 202
|
|
|
|
# act
|
|
response = _trigger(api, task, [p1])
|
|
|
|
# assert — the double-submit is refused; still one sub_task, one message.
|
|
assert response.status_code == 409
|
|
assert len(api.subtask_inputs(task)) == 1
|
|
assert len(api.sent_bodies) == 1
|