# retoor from devplacepy.database import get_table from tests.api.sync.conftest import comment_record, post_record, sync_ok, unique, user_record def listing(client, admin, origin, **params): return client.get("/sync/deletions", params={"origin": origin, **params}, headers=admin["headers"]) def ack(client, admin, origin, items): return client.post("/sync/deletions/ack", json={"origin": origin, "items": items}, headers=admin["headers"]) def seed(client, admin, origin): sync_ok(client, admin, "users", origin, [user_record("u1", unique("del"))]) sync_ok(client, admin, "posts", origin, [post_record("p1", "u1"), post_record("p2", "u1"), post_record("p3", "u1")]) sync_ok(client, admin, "comments", origin, [comment_record("c1", "p1", "u1")]) def local(origin, kind, origin_uid): return get_table("sync_links").find_one(origin=origin, kind=kind, origin_uid=origin_uid)["local_uid"] def test_authz(client, admin): from tests.api.sync.conftest import make_account assert client.get("/sync/deletions", params={"origin": "a.b"}).status_code == 401 assert listing(client, make_account("Member"), "a.b").status_code == 403 assert client.get("/sync/deletions", headers=admin["headers"]).status_code == 422 def test_nothing_is_listed_for_a_clean_origin_and_sync_own_deletions_never_are(client, admin, origin): seed(client, admin, origin) assert listing(client, admin, origin).json()["items"] == [] sync_ok(client, admin, "posts", origin, [post_record("p1", "u1", updated_at="2025-05-01T00:00:00Z", deleted=True)]) assert listing(client, admin, origin).json()["items"] == [] def test_a_member_deletion_a_system_deletion_and_a_purge_are_told_apart(client, admin, origin): seed(client, admin, origin) get_table("posts").update({"uid": local(origin, "posts", "p1"), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "someone"}, ["uid"]) get_table("posts").update({"uid": local(origin, "posts", "p2"), "deleted_at": "2026-01-02T00:00:00+00:00", "deleted_by": "system"}, ["uid"]) purged_uid = local(origin, "posts", "p3") get_table("posts").delete(uid=purged_uid) body = listing(client, admin, origin).json() found = {(item["kind"], item["origin_uid"]): item["by"] for item in body["items"]} assert found == {("posts", "p1"): "member", ("posts", "p2"): "system", ("posts", "p3"): "purged"} assert body["count"] == 3 and body["has_more"] is False def test_paging_with_a_cursor_covers_every_entry_once(client, admin, origin): seed(client, admin, origin) for origin_uid in ("p1", "p2", "p3"): get_table("posts").update({"uid": local(origin, "posts", origin_uid), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "m"}, ["uid"]) get_table("comments").update({"uid": local(origin, "comments", "c1"), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "m"}, ["uid"]) seen, cursor = [], "" while True: body = listing(client, admin, origin, limit=2, cursor=cursor).json() seen += [(item["kind"], item["origin_uid"]) for item in body["items"]] cursor = body["next_cursor"] if not body["has_more"]: break assert sorted(seen) == [("comments", "c1"), ("posts", "p1"), ("posts", "p2"), ("posts", "p3")] assert listing(client, admin, origin, cursor="zz").status_code == 400 def test_other_origins_are_never_listed(client, admin, origin): seed(client, admin, origin) get_table("posts").update({"uid": local(origin, "posts", "p1"), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "m"}, ["uid"]) assert listing(client, admin, f"{unique('x')}.example.org").json()["items"] == [] def test_acknowledging_stops_the_listing_and_quiets_the_resend(client, admin, origin): seed(client, admin, origin) purged = local(origin, "posts", "p3") get_table("posts").delete(uid=purged) resend = sync_ok(client, admin, "posts", origin, [post_record("p3", "u1", updated_at="2025-09-01T00:00:00Z")])["results"][0] assert resend["status"] == "rejected" and resend["reason"] == "purged_locally" response = ack(client, admin, origin, [{"kind": "posts", "origin_uid": "p3"}]) assert response.status_code == 200 and response.json()["acknowledged"] == 1 assert listing(client, admin, origin).json()["items"] == [] again = sync_ok(client, admin, "posts", origin, [post_record("p3", "u1", updated_at="2025-10-01T00:00:00Z")])["results"][0] assert again["status"] == "unchanged" and again["reason"] == "acknowledged_deleted" assert ack(client, admin, origin, [{"kind": "posts", "origin_uid": "p3"}]).json()["acknowledged"] == 0 def test_a_locally_deleted_row_after_ack_answers_acknowledged(client, admin, origin): seed(client, admin, origin) get_table("posts").update({"uid": local(origin, "posts", "p1"), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "m"}, ["uid"]) ack(client, admin, origin, [{"kind": "posts", "origin_uid": "p1"}]) result = sync_ok(client, admin, "posts", origin, [post_record("p1", "u1", content="edited after local delete", updated_at="2025-09-01T00:00:00Z")])["results"][0] assert result["status"] == "unchanged" and result["reason"] == "acknowledged_deleted" def test_ack_validation_and_audit(client, admin, origin): assert ack(client, admin, origin, []).status_code == 422 assert ack(client, admin, origin, [{"kind": "gists", "origin_uid": "x"}]).status_code == 422 assert ack(client, admin, origin, [{"kind": "posts", "origin_uid": "bad id"}]).status_code == 422 assert ack(client, admin, origin, [{"kind": "posts", "origin_uid": "unknown-one"}]).json()["acknowledged"] == 0 rows = list(get_table("audit_log").find(event_key="sync.deletions.ack", actor_uid=admin["uid"])) assert rows def test_a_purged_user_is_listed(client, admin, origin): seed(client, admin, origin) get_table("users").delete(uid=local(origin, "users", "u1")) items = listing(client, admin, origin).json()["items"] assert ("users", "u1", "purged") in {(item["kind"], item["origin_uid"], item["by"]) for item in items}