64388b75bf
End-to-end coverage of the tenancy/people flow and the privacy seam (private-tree isolation, public-tree view-but-not-edit, duplicate-email conflict, auth-required). DB-backed tests run against TEST_DATABASE_URL and skip cleanly when it is unset, so the no-DB suite still runs anywhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
"""End-to-end coverage of the core data model through the API: tenancy, the
|
|
privacy seam, and the pre-auth actor shim."""
|
|
|
|
|
|
async def _create_user(client, email: str) -> str:
|
|
resp = await client.post(
|
|
"/api/v1/users", json={"email": email, "display_name": email}
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
return resp.json()["id"]
|
|
|
|
|
|
async def test_tree_and_person_flow(client):
|
|
user_id = await _create_user(client, "keeper@example.com")
|
|
headers = {"X-User-Id": user_id}
|
|
|
|
resp = await client.post(
|
|
"/api/v1/trees", json={"name": "Smith Family", "visibility": "private"}, headers=headers
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
tree = resp.json()
|
|
assert tree["visibility"] == "private"
|
|
assert tree["owner_id"] == user_id
|
|
tree_id = tree["id"]
|
|
|
|
resp = await client.get("/api/v1/trees", headers=headers)
|
|
assert resp.status_code == 200
|
|
assert len(resp.json()) == 1
|
|
|
|
resp = await client.post(
|
|
f"/api/v1/trees/{tree_id}/persons",
|
|
json={"given": "John", "surname": "Smith"},
|
|
headers=headers,
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
person = resp.json()
|
|
assert person["primary_name"] == "John Smith"
|
|
assert person["tree_id"] == tree_id
|
|
|
|
resp = await client.get(f"/api/v1/trees/{tree_id}/persons", headers=headers)
|
|
assert resp.status_code == 200
|
|
assert len(resp.json()) == 1
|
|
|
|
|
|
async def test_private_tree_isolated_from_other_users(client):
|
|
owner = await _create_user(client, "owner@example.com")
|
|
other = await _create_user(client, "stranger@example.com")
|
|
|
|
resp = await client.post(
|
|
"/api/v1/trees", json={"name": "Private", "visibility": "private"},
|
|
headers={"X-User-Id": owner},
|
|
)
|
|
tree_id = resp.json()["id"]
|
|
|
|
# A non-member cannot view a private tree, nor list its people.
|
|
resp = await client.get(f"/api/v1/trees/{tree_id}", headers={"X-User-Id": other})
|
|
assert resp.status_code == 403
|
|
resp = await client.get(f"/api/v1/trees/{tree_id}/persons", headers={"X-User-Id": other})
|
|
assert resp.status_code == 403
|
|
|
|
|
|
async def test_public_tree_viewable_but_not_editable_by_non_member(client):
|
|
owner = await _create_user(client, "owner2@example.com")
|
|
viewer = await _create_user(client, "viewer2@example.com")
|
|
|
|
resp = await client.post(
|
|
"/api/v1/trees", json={"name": "Public", "visibility": "public"},
|
|
headers={"X-User-Id": owner},
|
|
)
|
|
tree_id = resp.json()["id"]
|
|
|
|
# Visible to a non-member...
|
|
resp = await client.get(f"/api/v1/trees/{tree_id}", headers={"X-User-Id": viewer})
|
|
assert resp.status_code == 200
|
|
# ...but not writable (not an editor).
|
|
resp = await client.post(
|
|
f"/api/v1/trees/{tree_id}/persons", json={"given": "Nope"},
|
|
headers={"X-User-Id": viewer},
|
|
)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
async def test_duplicate_email_conflicts(client):
|
|
await _create_user(client, "dupe@example.com")
|
|
resp = await client.post("/api/v1/users", json={"email": "dupe@example.com"})
|
|
assert resp.status_code == 409
|
|
|
|
|
|
async def test_auth_required_without_header(client):
|
|
resp = await client.get("/api/v1/trees")
|
|
assert resp.status_code == 401
|