Newer
Older
navi-1 / tests / unit / api / test_synapse.py
"""Synapse s2s gateway tests — signature verification and target CRUD."""

import json
from unittest.mock import AsyncMock, MagicMock

import pytest
from fastapi.testclient import TestClient

from navi.config import Settings

S2S_SECRET = "s2s-target-secret-0123456789"


@pytest.fixture
def synapse_client(monkeypatch):
    import navi.api.routes.synapse as synapse_mod
    import navi.auth.deps as auth_deps

    # Anonymous admin (auth disabled) — CRUD routes resolve without a cookie.
    monkeypatch.setattr(
        auth_deps, "settings",
        Settings(_env_file=None, navi_persona_file="", navi_auth_enabled=False),
    )
    monkeypatch.setattr(synapse_mod, "_pool", AsyncMock(return_value=MagicMock()))

    from navi.main import app

    return TestClient(app)


def _target(**overrides):
    target = {"id": 1, "user_id": "u1", "token_ref": "tgt-1", "secret": S2S_SECRET}
    target.update(overrides)
    return target


def _sign(body: bytes, secret: str = S2S_SECRET):
    from gnexus_synapse import make_signature

    return {"X-Gnexus-Signature": make_signature(body, secret),
            "X-Gnexus-Event-Type": "gntodo.task.created"}


def _post(tc: TestClient, url: str, body: dict, secret: str = S2S_SECRET):
    raw = json.dumps(body).encode()
    return tc.post(url, content=raw, headers=_sign(raw, secret))


def test_synapse_delivery_503_without_targets(synapse_client, monkeypatch):
    import navi.api.routes.synapse as synapse_mod

    monkeypatch.setattr(
        synapse_mod.SynapseTargetStore, "active_secrets",
        AsyncMock(return_value=[]),
    )
    envelope = {"event_id": "e1", "subject": "topic", "action": "done"}
    resp = _post(synapse_client, "/webhooks/synapse", envelope)
    assert resp.status_code == 503


def test_synapse_delivery_401_on_foreign_secret(synapse_client, monkeypatch):
    import navi.api.routes.synapse as synapse_mod

    monkeypatch.setattr(
        synapse_mod.SynapseTargetStore, "active_secrets",
        AsyncMock(return_value=[_target()]),
    )
    envelope = {"event_id": "e2", "subject": "topic", "action": "done"}
    resp = _post(synapse_client, "/webhooks/synapse", envelope, secret="wrong-secret-0123")
    assert resp.status_code == 401


def test_synapse_delivery_401_on_broken_json(synapse_client, monkeypatch):
    """A correctly signed non-JSON body must not leak a 500 — it is a 401."""
    import navi.api.routes.synapse as synapse_mod

    monkeypatch.setattr(
        synapse_mod.SynapseTargetStore, "active_secrets",
        AsyncMock(return_value=[_target()]),
    )
    resp = synapse_client.post(
        "/webhooks/synapse", content=b"not-json{", headers=_sign(b"not-json{"),
    )
    assert resp.status_code == 401


def test_synapse_delivery_200_and_both_spellings(synapse_client, monkeypatch):
    import navi.api.routes.synapse as synapse_mod

    monkeypatch.setattr(
        synapse_mod.SynapseTargetStore, "active_secrets",
        AsyncMock(return_value=[_target()]),
    )
    envelope = {
        "event_id": "e3", "subject": "task", "action": "completed",
        "payload": {"user_id": "u1"}, "source": "gntodo",
    }
    client = synapse_client
    resp = _post(client, "/webhooks/synapse", envelope)
    assert resp.status_code == 200
    assert resp.json() == {"received": True, "event_id": "e3"}

    resp_slash = _post(client, "/webhooks/synapse/", envelope)
    assert resp_slash.status_code == 200


def test_synapse_duplicate_delivery_acknowledged_once(synapse_client, monkeypatch):
    """A replayed event id is acked again but only marked once."""
    import navi.api.routes.synapse as synapse_mod

    monkeypatch.setattr(
        synapse_mod.SynapseTargetStore, "active_secrets",
        AsyncMock(return_value=[_target()]),
    )
    synapse_mod._seen_events.clear()

    envelope = {"event_id": "e4", "subject": "t", "action": "a"}
    client = synapse_client
    first = _post(client, "/webhooks/synapse", envelope)
    assert first.status_code == 200

    second = _post(client, "/webhooks/synapse", envelope)
    assert second.status_code == 200
    assert synapse_mod._seen_events["e4"] is True


def test_synapse_target_crud(synapse_client, monkeypatch):
    import navi.api.routes.synapse as synapse_mod
    import navi.synapse.store as store_mod

    created = MagicMock(
        id=7, token_ref="tgt-9",
        created_at=__import__("datetime").datetime(2026, 10, 7, 12, 0, 0),
    )
    monkeypatch.setattr(store_mod.SynapseTargetStore, "create", AsyncMock(return_value=created))
    monkeypatch.setattr(store_mod.SynapseTargetStore, "list_for_user", AsyncMock(return_value=[created]))
    monkeypatch.setattr(store_mod.SynapseTargetStore, "revoke", AsyncMock(return_value=True))

    client = synapse_client
    created_resp = client.post(
        "/synapse-targets",
        json={"token_ref": "tgt-9", "secret": S2S_SECRET},
    )
    assert created_resp.status_code == 200
    assert created_resp.json()["token_ref"] == "tgt-9"

    listed = client.get("/synapse-targets")
    assert listed.status_code == 200
    assert listed.json()["items"][0]["token_ref"] == "tgt-9"

    revoked = client.delete("/synapse-targets/7")
    assert revoked.status_code == 204