"""Tests for the dispatch participant client and HTTP inbox.""" from __future__ import annotations import importlib import os import pytest from fastapi.testclient import TestClient from app.integrations import dispatch_client def test_build_envelope_matches_event_card_profile(): env = dispatch_client.build_envelope("LegacyhubDocumentIndexed", "card-1", {"x": 1}, event_id="e1") assert env["message_type"] == "LegacyhubDocumentIndexed" assert env["event_id"] == "e1" assert env["participant_code"] == "legacyhub" assert "message_id" in env and "created_at" in env # Doc 23 event profile against the platform pydantic model: UUID card_uid, # int task_id/state_code/schema_version, payload under card.event. import uuid as _uuid _uuid.UUID(env["card_uid"]) # non-UUID business id mapped to stable uuid5 assert env["task_id"] == 0 card = env["card"] assert card["card_uid"] == env["card_uid"] assert card["schema_version"] == 1 assert card["task_id"] == 0 assert card["state_code"] == 0 assert card["specialist_status"] == "n/a" assert isinstance(card["sequence_number"], int) assert 0 < card["sequence_number"] < 2**31 # int4 column in dispatch-db assert card["created_at"] == env["created_at"] == card["updated_at"] assert card["event"] == {"x": 1} assert env["media_files"] == [] assert env["media_file_count"] == 0 def test_card_uid_stable_for_non_uuid_ids(): a = dispatch_client.build_envelope("E", "asset_9", {})["card_uid"] b = dispatch_client.build_envelope("E", "asset_9", {})["card_uid"] c = dispatch_client.build_envelope("E", "asset_10", {})["card_uid"] assert a == b != c passthrough = "0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0" assert dispatch_client.build_envelope("E", passthrough, {})["card_uid"] == passthrough def test_deterministic_event_id_is_stable_and_distinct(): a = dispatch_client.deterministic_event_id("LegacyhubDocumentIndexed", "asset_1") b = dispatch_client.deterministic_event_id("LegacyhubDocumentIndexed", "asset_1") c = dispatch_client.deterministic_event_id("LegacyhubDocumentIndexed", "asset_2") assert a == b assert a != c def test_publish_noop_when_disabled(monkeypatch): # Patch the settings object the module actually holds (it binds settings at # import; other tests may have reloaded app.config to a different instance). monkeypatch.setattr(dispatch_client.settings, "dispatch_enabled", False) assert dispatch_client.publish("X", "card", {}) is None class _FakeResp: def raise_for_status(self) -> None: pass class _FakeHttpx: def __init__(self) -> None: self.calls: list[dict] = [] def post(self, url, json, headers, timeout): # noqa: A002 - mirror httpx signature self.calls.append({"url": url, "json": json, "headers": headers}) return _FakeResp() def test_publish_posts_when_enabled(monkeypatch): monkeypatch.setattr(dispatch_client.settings, "dispatch_enabled", True) monkeypatch.setattr(dispatch_client.settings, "dispatch_api_url", "http://dispatch-api:47137") monkeypatch.setattr(dispatch_client.settings, "dispatch_api_key", "k") fake = _FakeHttpx() monkeypatch.setattr(dispatch_client, "httpx", fake) env = dispatch_client.publish("LegacyhubDocumentIndexed", "card-1", {"x": 1}, event_id="e1") assert env is not None assert len(fake.calls) == 1 assert fake.calls[0]["url"].endswith("/dispatch/envelopes") assert fake.calls[0]["headers"]["X-API-Key"] == "k" def test_publish_document_indexed_uses_asset_id_as_card(monkeypatch): monkeypatch.setattr(dispatch_client.settings, "dispatch_enabled", True) monkeypatch.setattr(dispatch_client.settings, "dispatch_api_url", "http://dispatch-api:47137") fake = _FakeHttpx() monkeypatch.setattr(dispatch_client, "httpx", fake) env = dispatch_client.publish_document_indexed( "doc-uuid", asset_metadata={"asset_id": "asset_9", "owner_module": "qms"} ) assert env is not None assert env["card_uid"] == dispatch_client._card_uid("asset_9") assert env["card"]["event"]["asset_id"] == "asset_9" assert env["card"]["event"]["owner_module"] == "qms" # ---- inbox route ---- @pytest.fixture def dispatch_app(monkeypatch): monkeypatch.setenv("INGEST_API_KEY", "svc-key") monkeypatch.delenv("API_KEY", raising=False) import app.config as cfg import app.main as main_module cfg.get_settings.cache_clear() importlib.reload(cfg) importlib.reload(main_module) yield main_module.app for _k in ("API_KEY", "INGEST_API_KEY", "AUTH_REQUIRE_IDENTITY"): os.environ.pop(_k, None) cfg.get_settings.cache_clear() importlib.reload(cfg) importlib.reload(main_module) _ENVELOPE = {"event_id": "evt-1", "message_type": "ReindexRequested", "body": {"asset_id": "a1"}} def test_inbox_rejects_without_service_key(dispatch_app): from app.config import settings client = TestClient(dispatch_app) res = client.post(f"{settings.app_api_prefix}/dispatch/inbox", json=_ENVELOPE) assert res.status_code == 401 def test_inbox_confirms_with_service_key(dispatch_app, monkeypatch): from app.api import routes_dispatch_inbox from app.config import settings monkeypatch.setattr( routes_dispatch_inbox, "handle_inbox_message", lambda env: {"status": "confirmed", "handled": True}, ) client = TestClient(dispatch_app) res = client.post( f"{settings.app_api_prefix}/dispatch/inbox", headers={"X-API-Key": "svc-key"}, json=_ENVELOPE, ) assert res.status_code == 200 assert res.json()["status"] == "confirmed" assert res.json()["handled"] is True assert res.json()["event_id"] == "evt-1"