d6e2df4a61
Events (create/list-per-person/soft-delete) and relationships (create/list-per-person/soft-delete) through the layered stack: editor-gated writes, privacy-engine reads, audit on every change. Events carry exactly one subject (person XOR partnership); relationships are typed qualified edges (parent_child gets a biological/adoptive/step/foster/donor/guardian qualifier). Adds a single-person GET. 18 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
117 lines
3.8 KiB
Python
117 lines
3.8 KiB
Python
"""Events and relationships through the API."""
|
|
|
|
from tests.conftest import auth, register
|
|
|
|
|
|
async def _setup_tree_with_two_people(client, email: str):
|
|
token = await register(client, email)
|
|
h = auth(token)
|
|
tree_id = (
|
|
await client.post("/api/v1/trees", json={"name": "Graph"}, headers=h)
|
|
).json()["id"]
|
|
parent = (
|
|
await client.post(
|
|
f"/api/v1/trees/{tree_id}/persons",
|
|
json={"given": "Anna", "surname": "Vogel"},
|
|
headers=h,
|
|
)
|
|
).json()["id"]
|
|
child = (
|
|
await client.post(
|
|
f"/api/v1/trees/{tree_id}/persons",
|
|
json={"given": "Beth", "surname": "Vogel"},
|
|
headers=h,
|
|
)
|
|
).json()["id"]
|
|
return h, tree_id, parent, child
|
|
|
|
|
|
async def test_event_create_list_delete(client):
|
|
h, tree_id, parent, _ = await _setup_tree_with_two_people(client, "ev1@example.com")
|
|
|
|
resp = await client.post(
|
|
f"/api/v1/trees/{tree_id}/events",
|
|
json={"event_type": "birth", "person_id": parent, "date_value": "ABT 1850"},
|
|
headers=h,
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
event_id = resp.json()["id"]
|
|
|
|
listed = await client.get(f"/api/v1/trees/{tree_id}/persons/{parent}/events", headers=h)
|
|
assert listed.status_code == 200
|
|
assert len(listed.json()) == 1
|
|
assert listed.json()[0]["event_type"] == "birth"
|
|
|
|
resp = await client.delete(f"/api/v1/trees/{tree_id}/events/{event_id}", headers=h)
|
|
assert resp.status_code == 204
|
|
listed = await client.get(f"/api/v1/trees/{tree_id}/persons/{parent}/events", headers=h)
|
|
assert len(listed.json()) == 0
|
|
|
|
|
|
async def test_event_requires_exactly_one_subject(client):
|
|
h, tree_id, _, _ = await _setup_tree_with_two_people(client, "ev2@example.com")
|
|
resp = await client.post(
|
|
f"/api/v1/trees/{tree_id}/events", json={"event_type": "birth"}, headers=h
|
|
)
|
|
assert resp.status_code == 409
|
|
|
|
|
|
async def test_relationship_create_and_list(client):
|
|
h, tree_id, parent, child = await _setup_tree_with_two_people(client, "rel1@example.com")
|
|
|
|
resp = await client.post(
|
|
f"/api/v1/trees/{tree_id}/relationships",
|
|
json={
|
|
"type": "parent_child",
|
|
"person_from_id": parent,
|
|
"person_to_id": child,
|
|
"qualifier": "biological",
|
|
},
|
|
headers=h,
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
|
|
for pid in (parent, child):
|
|
listed = await client.get(
|
|
f"/api/v1/trees/{tree_id}/persons/{pid}/relationships", headers=h
|
|
)
|
|
assert listed.status_code == 200
|
|
assert len(listed.json()) == 1
|
|
assert listed.json()[0]["qualifier"] == "biological"
|
|
|
|
|
|
async def test_relationship_validation(client):
|
|
h, tree_id, parent, _ = await _setup_tree_with_two_people(client, "rel2@example.com")
|
|
# Same person on both ends.
|
|
resp = await client.post(
|
|
f"/api/v1/trees/{tree_id}/relationships",
|
|
json={"type": "sibling", "person_from_id": parent, "person_to_id": parent},
|
|
headers=h,
|
|
)
|
|
assert resp.status_code == 409
|
|
|
|
# Qualifier on a non-parent_child edge.
|
|
h2, t2, p_a, p_b = await _setup_tree_with_two_people(client, "rel3@example.com")
|
|
resp = await client.post(
|
|
f"/api/v1/trees/{t2}/relationships",
|
|
json={
|
|
"type": "partnership",
|
|
"person_from_id": p_a,
|
|
"person_to_id": p_b,
|
|
"qualifier": "biological",
|
|
},
|
|
headers=h2,
|
|
)
|
|
assert resp.status_code == 409
|
|
|
|
|
|
async def test_non_member_cannot_write_graph(client):
|
|
h, tree_id, parent, _ = await _setup_tree_with_two_people(client, "rel4@example.com")
|
|
other = auth(await register(client, "intruder@example.com"))
|
|
resp = await client.post(
|
|
f"/api/v1/trees/{tree_id}/events",
|
|
json={"event_type": "birth", "person_id": parent},
|
|
headers=other,
|
|
)
|
|
assert resp.status_code == 403
|