"""Per-topology persona endpoints — GET/PUT /topologies/{id}/personas.""" from __future__ import annotations import json import pytest from decnet.topology.config import TopologyConfig from decnet.topology.generator import generate from decnet.topology.persistence import persist from decnet.web.dependencies import repo as _repo _V1 = "/api/v1/topologies" def _cfg(name: str = "personas") -> TopologyConfig: return TopologyConfig( name=name, depth=1, branching_factor=1, deckies_per_lan_min=1, deckies_per_lan_max=1, services_explicit=["ssh"], randomize_services=False, seed=0, ) async def _seed(name: str = "personas") -> str: return await persist(_repo, generate(_cfg(name))) def _persona(email: str, name: str = "Jane Doe") -> dict: return { "name": name, "email": email, "role": "Admin", "tone": "formal", "mannerisms": ["uses bullet points"], } @pytest.mark.anyio async def test_get_default_empty(client, auth_token): tid = await _seed("get-empty") r = await client.get( f"{_V1}/{tid}/personas", headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 200, r.text body = r.json() assert body["topology_id"] == tid assert body["personas"] == [] assert body["language_default"] == "en" @pytest.mark.anyio async def test_get_404(client, auth_token): r = await client.get( f"{_V1}/does-not-exist/personas", headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 404 @pytest.mark.anyio async def test_put_then_get(client, auth_token): tid = await _seed("put-roundtrip") payload = {"personas": [ _persona("a@example.com", "Alice"), _persona("b@example.com", "Bob"), ]} r = await client.put( f"{_V1}/{tid}/personas", json=payload, headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 200, r.text assert len(r.json()["personas"]) == 2 r2 = await client.get( f"{_V1}/{tid}/personas", headers={"Authorization": f"Bearer {auth_token}"}, ) assert r2.status_code == 200 emails = [p["email"] for p in r2.json()["personas"]] assert emails == ["a@example.com", "b@example.com"] # Persisted as JSON string in the topology row. topo = await _repo.get_topology(tid) assert isinstance(topo["email_personas"], str) stored = json.loads(topo["email_personas"]) assert {p["email"] for p in stored} == {"a@example.com", "b@example.com"} @pytest.mark.anyio async def test_put_empty_clears(client, auth_token): tid = await _seed("put-empty") await client.put( f"{_V1}/{tid}/personas", json={"personas": [_persona("x@example.com")]}, headers={"Authorization": f"Bearer {auth_token}"}, ) r = await client.put( f"{_V1}/{tid}/personas", json={"personas": []}, headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 200 assert r.json()["personas"] == [] @pytest.mark.anyio async def test_put_non_list_400(client, auth_token): tid = await _seed("put-non-list") r = await client.put( f"{_V1}/{tid}/personas", json={"personas": "not a list"}, headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 400 @pytest.mark.anyio async def test_put_all_invalid_400(client, auth_token): tid = await _seed("put-all-bad") r = await client.put( f"{_V1}/{tid}/personas", json={"personas": [{"email": "no-at-sign"}, {"name": "no-email"}]}, headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 400 @pytest.mark.anyio async def test_put_partial_invalid_keeps_valid(client, auth_token): """Mirror the global-pool drop-invalid semantics. The endpoint silently drops bad entries; operators discover what landed by reading back the GET. """ tid = await _seed("put-partial") r = await client.put( f"{_V1}/{tid}/personas", json={"personas": [ _persona("good@example.com"), {"name": "missing email"}, ]}, headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 200 body = r.json() assert [p["email"] for p in body["personas"]] == ["good@example.com"] @pytest.mark.anyio async def test_put_404_on_missing_topology(client, auth_token): r = await client.put( f"{_V1}/does-not-exist/personas", json={"personas": [_persona("x@example.com")]}, headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 404 @pytest.mark.anyio async def test_get_does_not_shadow_existing_topology_id(client, auth_token): """Ensure the personas subroute is registered before the bare /{id}. If the literal `/personas` segment got shadowed by the parameterized `/{id}` route, GET would return the topology body instead of 404 for a missing personas resource. Sanity-check the order. """ tid = await _seed("shadow-check") r = await client.get( f"{_V1}/{tid}/personas", headers={"Authorization": f"Bearer {auth_token}"}, ) assert r.status_code == 200 assert "personas" in r.json()