Files
provenance/backend/tests/test_relationship_dedup.py
T
justin fae1162ff8 Prevent duplicate relationships; harden tree render against bad graphs
Root cause of the blank Jung tree: a child double-linked to the same parent
(and, generally, any cycle) made family-chart recurse forever.

Backend (the real fix):
- create_relationship now rejects an equivalent existing edge → 409.
  parent_child is directional (parent→child); partnership/sibling match the
  pair in either order. So you can't link the same two people the same way
  twice. (GEDCOM import already deduped; manual creates didn't.)

Frontend (defense in depth so data can never blank the view):
- Tree view sanitizes the graph before rendering: dedupes parents/spouses,
  drops self-links, and greedily breaks ancestor cycles (a person can't be
  their own ancestor); children are derived from the kept edges. The render is
  wrapped in try/catch and shows a note instead of a blank canvas, telling you
  which conflicting links were skipped.
- Person page surfaces the 409 ("They're already linked that way.").

59 backend tests pass (incl. dup-rejection + reverse-parent-child allowed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:35:11 -04:00

66 lines
2.3 KiB
Python

"""Duplicate relationships are rejected (no double-linking)."""
from tests.conftest import auth, register
async def _setup(client, email):
h = auth(await register(client, email))
tid = (await client.post("/api/v1/trees", json={"name": "T"}, headers=h)).json()["id"]
async def person(given):
return (
await client.post(f"/api/v1/trees/{tid}/persons", json={"given": given}, headers=h)
).json()["id"]
return h, tid, person
async def test_duplicate_parent_child_rejected(client):
h, tid, person = await _setup(client, "dup-pc@example.com")
karl = await person("Karl")
kid = await person("Kid")
body = {"type": "parent_child", "person_from_id": karl, "person_to_id": kid}
first = await client.post(f"/api/v1/trees/{tid}/relationships", json=body, headers=h)
assert first.status_code == 201
dup = await client.post(f"/api/v1/trees/{tid}/relationships", json=body, headers=h)
assert dup.status_code == 409
async def test_duplicate_partnership_either_direction_rejected(client):
h, tid, person = await _setup(client, "dup-sp@example.com")
a = await person("A")
b = await person("B")
first = await client.post(
f"/api/v1/trees/{tid}/relationships",
json={"type": "partnership", "person_from_id": a, "person_to_id": b},
headers=h,
)
assert first.status_code == 201
# Same couple, reversed order — still a duplicate.
dup = await client.post(
f"/api/v1/trees/{tid}/relationships",
json={"type": "partnership", "person_from_id": b, "person_to_id": a},
headers=h,
)
assert dup.status_code == 409
async def test_reverse_parent_child_is_allowed(client):
"""A->B as parent_child shouldn't block B->A (different meaning)."""
h, tid, person = await _setup(client, "dup-rev@example.com")
a = await person("A")
b = await person("B")
r1 = await client.post(
f"/api/v1/trees/{tid}/relationships",
json={"type": "parent_child", "person_from_id": a, "person_to_id": b},
headers=h,
)
r2 = await client.post(
f"/api/v1/trees/{tid}/relationships",
json={"type": "parent_child", "person_from_id": b, "person_to_id": a},
headers=h,
)
assert r1.status_code == 201 and r2.status_code == 201