import uuid from app.core.config import get_settings def _callback_headers(event_id: str, token: str | None = None): settings = get_settings() return { "Idempotency-Key": event_id, "X-Service-Token": token if token is not None else settings.n8n_callback_token, } def test_callback_rejects_wrong_service_token(client): response = client.post( "/api/v1/integrations/n8n/return-callback", json={"follow_up": "cleaning"}, headers=_callback_headers(str(uuid.uuid4()), token="wrong-token"), ) assert response.status_code == 401 def test_callback_unknown_event_returns_404(client): response = client.post( "/api/v1/integrations/n8n/return-callback", json={"follow_up": "cleaning"}, headers=_callback_headers(str(uuid.uuid4())), ) assert response.status_code == 404 def test_callback_is_idempotent_by_event_id(client, ops_client): from sqlalchemy import select from app.core.db import SessionLocal from app.models.booking import Booking from app.models.outbox import OutboxEvent db = SessionLocal() try: booking = db.scalar(select(Booking).limit(1)) event = OutboxEvent( event_id=uuid.uuid4(), event_type="vehicle.returned.v1", aggregate_type="booking", aggregate_id=booking.id, payload_json={ "correlation_id": str(uuid.uuid4()), "aggregate": { "type": "booking", "id": str(booking.id), "public_ref": booking.public_ref, }, "data": {}, "aggregate_ref": booking.public_ref, }, occurred_at=db.execute(select(Booking.starts_at).limit(1)).scalar(), delivery_status="delivering", attempts=1, ) db.add(event) db.commit() event_id = str(event.event_id) finally: db.close() first = client.post( "/api/v1/integrations/n8n/return-callback", json={"follow_up": "cleaning", "summary": "test"}, headers=_callback_headers(event_id), ) second = client.post( "/api/v1/integrations/n8n/return-callback", json={"follow_up": "cleaning", "summary": "test"}, headers=_callback_headers(event_id), ) assert first.status_code == 200 assert second.status_code == 200 audit_events = ops_client.get( "/api/v1/audit", params={"action": "n8n_return_followup_recorded"} ).json() matching = [e for e in audit_events if e["metadata"]["event_id"] == event_id] assert len(matching) == 1 def test_scheduled_scan_rejects_wrong_service_token(client): response = client.post( "/api/v1/integrations/n8n/scheduled-scan", headers={"X-Service-Token": "wrong-token"}, ) assert response.status_code == 401 def test_scheduled_scan_requires_service_token_header(client): response = client.post("/api/v1/integrations/n8n/scheduled-scan") assert response.status_code == 422 def test_scheduled_scan_runs_and_returns_counts_by_rule(client, ops_client): settings = get_settings() response = client.post( "/api/v1/integrations/n8n/scheduled-scan", headers={"X-Service-Token": settings.n8n_callback_token}, ) assert response.status_code == 200 assert response.json() == {"created": {}} # already-seeded conditions, nothing new audit_events = ops_client.get( "/api/v1/audit", params={"action": "data_quality_scan_run"} ).json() service_triggered = [e for e in audit_events if e["actor_type"] == "service"] assert len(service_triggered) >= 1 assert service_triggered[0]["actor_label"] == "n8n scheduled scan" def test_scheduled_scan_is_idempotent_across_repeated_triggers(client): settings = get_settings() headers = {"X-Service-Token": settings.n8n_callback_token} first = client.post("/api/v1/integrations/n8n/scheduled-scan", headers=headers) second = client.post("/api/v1/integrations/n8n/scheduled-scan", headers=headers) assert first.status_code == 200 assert second.status_code == 200 assert second.json()["created"] == {}