Add events and relationships API (Phase 1: flesh out the graph)
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>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user