"""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