Zo doen wijdat in oost

This commit is contained in:
2026-10-16 07:20:44 +02:00
parent 25d9cb1c7d
commit 6f80a820da
229 changed files with 23894 additions and 74 deletions
+1
View File
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
+70
View File
@@ -0,0 +1,70 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_table
from tests.api.sync.conftest import comment_record, post_record, sync_ok, unique, user_record
def seed_thread(client, admin, origin):
sync_ok(client, admin, "users", origin, [user_record("u1", unique("cu1")), user_record("u2", unique("cu2"))])
sync_ok(client, admin, "posts", origin, [post_record("p1", "u1"), post_record("p2", "u1")])
def test_unknown_references_are_rejected_with_specific_reasons(client, admin, origin):
seed_thread(client, admin, origin)
body = sync_ok(
client, admin, "comments", origin,
[
comment_record("c1", author="nobody"),
comment_record("c2", post="nopost"),
comment_record("c3", parent_uid="noparent"),
],
)
assert [r["reason"] for r in body["results"]] == ["unknown_author", "unknown_post", "unknown_parent"]
def test_create_with_nested_reply_in_one_batch(client, admin, origin):
seed_thread(client, admin, origin)
body = sync_ok(
client, admin, "comments", origin,
[comment_record("c1", author="u2"), comment_record("c2", author="u1", parent_uid="c1")],
)
assert [r["status"] for r in body["results"]] == ["created", "created"]
parent, reply = (get_table("comments").find_one(uid=r["local_uid"]) for r in body["results"])
post = get_table("posts").find_one(uid=reply["post_uid"])
assert reply["parent_uid"] == parent["uid"]
assert reply["target_type"] == "post" and reply["target_uid"] == post["uid"]
assert parent["deleted_at"] is None
def test_parent_on_a_different_post_is_rejected(client, admin, origin):
seed_thread(client, admin, origin)
sync_ok(client, admin, "comments", origin, [comment_record("c1", post="p1")])
body = sync_ok(client, admin, "comments", origin, [comment_record("c2", post="p2", parent_uid="c1")])
assert body["results"][0]["reason"] == "parent_mismatch"
def test_edit_replay_and_tombstone(client, admin, origin):
seed_thread(client, admin, origin)
created = sync_ok(client, admin, "comments", origin, [comment_record("c1")])["results"][0]
assert sync_ok(client, admin, "comments", origin, [comment_record("c1")])["results"][0]["status"] == "unchanged"
edited = sync_ok(
client, admin, "comments", origin,
[comment_record("c1", content="edited comment", updated_at="2025-02-01T00:00:00Z")],
)["results"][0]
assert edited["status"] == "updated"
assert get_table("comments").find_one(uid=created["local_uid"])["content"] == "edited comment"
sync_ok(
client, admin, "comments", origin,
[comment_record("c1", content="edited comment", updated_at="2025-03-01T00:00:00Z", deleted=True)],
)
row = get_table("comments").find_one(uid=created["local_uid"])
assert row["deleted_at"] and row["deleted_by"] == "sync"
def test_synced_comment_renders_on_the_post_page(client, admin, origin):
seed_thread(client, admin, origin)
sync_ok(client, admin, "comments", origin, [comment_record("c1", content="comment visible on the page")])
link = get_table("sync_links").find_one(origin=origin, kind="posts", origin_uid="p1")
slug = get_table("posts").find_one(uid=link["local_uid"])["slug"]
page = client.get(f"/posts/{slug}")
assert page.status_code == 200 and "comment visible on the page" in page.text
+98
View File
@@ -0,0 +1,98 @@
# retoor <retoor@molodetz.nl>
import time
import pytest
from fastapi.testclient import TestClient
from devplacepy.database import get_setting, get_table, set_setting
from devplacepy.main import app
from devplacepy.utils import register_account
_counter = [0]
def unique(prefix: str) -> str:
_counter[0] += 1
return f"{prefix}{int(time.time() * 1000)}{_counter[0]}"
def make_account(role: str = "Member") -> dict:
name = unique("syncmb")
uid, _, _ = register_account(
name, f"{name}@t.dev", "secret123", age_band="adult", accepted_terms=True
)
if role != "Member":
get_table("users").update({"uid": uid, "role": role}, ["uid"])
row = get_table("users").find_one(uid=uid)
return {
"uid": uid,
"username": name,
"headers": {"X-API-KEY": row["api_key"], "Accept": "application/json"},
}
@pytest.fixture(scope="module")
def client():
previous = {key: get_setting(key, "") for key in ("sync_enabled", "sync_origins")}
set_setting("sync_enabled", "1")
set_setting("sync_origins", "")
with TestClient(app) as test_client:
yield test_client
for key, value in previous.items():
set_setting(key, value)
@pytest.fixture
def admin(client):
return make_account("Admin")
@pytest.fixture
def origin():
return f"{unique('o')}.example.org"
def sync(client, admin, kind, origin, records, **extra):
response = client.post(
f"/sync/{kind}",
json={"origin": origin, "records": records, **extra},
headers=admin["headers"],
)
return response
def sync_ok(client, admin, kind, origin, records, **extra) -> dict:
response = sync(client, admin, kind, origin, records, **extra)
assert response.status_code == 200, response.text
return response.json()
def user_record(uid="u1", username="syncuser", **extra) -> dict:
return {
"origin_uid": uid,
"username": username,
"created_at": "2025-01-01T00:00:00Z",
**extra,
}
def post_record(uid="p1", author="u1", **extra) -> dict:
return {
"origin_uid": uid,
"author_uid": author,
"content": "a synced post with enough content",
"created_at": "2025-01-02T00:00:00Z",
**extra,
}
def comment_record(uid="c1", post="p1", author="u1", **extra) -> dict:
return {
"origin_uid": uid,
"post_uid": post,
"author_uid": author,
"content": "a synced comment",
"created_at": "2025-01-03T00:00:00Z",
**extra,
}
+118
View File
@@ -0,0 +1,118 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_table
from tests.api.sync.conftest import post_record, sync_ok, unique, user_record
def seed_author(client, admin, origin, uid="u1"):
body = sync_ok(client, admin, "users", origin, [user_record(uid, unique("author"))])
return body["results"][0]["local_uid"]
def test_unknown_author_is_rejected_with_reason(client, admin, origin):
body = sync_ok(client, admin, "posts", origin, [post_record(author="missing")])
result = body["results"][0]
assert result["status"] == "rejected" and result["reason"] == "unknown_author"
assert get_table("sync_links").count(origin=origin) == 0
def test_create_normalizes_and_has_no_side_effects(client, admin, origin):
author = seed_author(client, admin, origin)
notifications_before = get_table("notifications").count()
body = sync_ok(
client, admin, "posts", origin,
[post_record("p1", title=" Hello World ", content="line one\r\nline two ok", topic="nonsense", stars=50)],
)
result = body["results"][0]
assert result["status"] == "created" and result["ignored"] == ["stars"]
row = get_table("posts").find_one(uid=result["local_uid"])
assert row["user_uid"] == author
assert row["title"] == "Hello World"
assert row["content"] == "line one\nline two ok"
assert row["topic"] == "random"
assert row["stars"] == 0
assert row["created_at"] == "2025-01-02T00:00:00+00:00"
assert row["slug"].endswith("hello-world")
assert row["deleted_at"] is None
assert get_table("notifications").count() == notifications_before
assert get_table("users").find_one(uid=author)["xp"] == 0
def test_replay_update_stale_and_tombstone(client, admin, origin):
seed_author(client, admin, origin)
created = sync_ok(client, admin, "posts", origin, [post_record("p1")])["results"][0]
assert sync_ok(client, admin, "posts", origin, [post_record("p1")])["results"][0]["status"] == "unchanged"
edited = sync_ok(
client, admin, "posts", origin,
[post_record("p1", content="edited synced content", updated_at="2025-03-01T00:00:00Z")],
)["results"][0]
assert edited["status"] == "updated" and edited["local_uid"] == created["local_uid"]
assert get_table("posts").find_one(uid=created["local_uid"])["content"] == "edited synced content"
stale = sync_ok(
client, admin, "posts", origin,
[post_record("p1", content="older content here", updated_at="2025-01-05T00:00:00Z")],
)["results"][0]
assert stale["status"] == "stale"
deleted = sync_ok(
client, admin, "posts", origin,
[post_record("p1", content="edited synced content", updated_at="2025-04-01T00:00:00Z", deleted=True)],
)["results"][0]
assert deleted["status"] == "updated"
row = get_table("posts").find_one(uid=created["local_uid"])
assert row["deleted_at"] and row["deleted_by"] == "sync"
revived = sync_ok(
client, admin, "posts", origin,
[post_record("p1", content="edited synced content", updated_at="2025-05-01T00:00:00Z")],
)["results"][0]
assert revived["status"] == "updated"
assert get_table("posts").find_one(uid=created["local_uid"])["deleted_at"] is None
def test_locally_moderated_post_is_not_resurrected(client, admin, origin):
seed_author(client, admin, origin)
created = sync_ok(client, admin, "posts", origin, [post_record("p1")])["results"][0]
get_table("posts").update(
{"uid": created["local_uid"], "deleted_at": "2025-02-01T00:00:00+00:00", "deleted_by": admin["uid"]},
["uid"],
)
again = sync_ok(
client, admin, "posts", origin,
[post_record("p1", content="remote edit arrives", updated_at="2025-03-01T00:00:00Z")],
)["results"][0]
assert again["status"] == "unchanged" and again["reason"] == "locally_deleted"
assert get_table("posts").find_one(uid=created["local_uid"])["content"] != "remote edit arrives"
def test_purged_local_copy_is_not_recreated(client, admin, origin):
seed_author(client, admin, origin)
created = sync_ok(client, admin, "posts", origin, [post_record("p1")])["results"][0]
get_table("posts").delete(uid=created["local_uid"])
again = sync_ok(
client, admin, "posts", origin,
[post_record("p1", content="changed after purge", updated_at="2025-03-01T00:00:00Z")],
)["results"][0]
assert again["status"] == "rejected" and again["reason"] == "purged_locally"
assert get_table("posts").count(uid=created["local_uid"]) == 0
def test_invalid_post_reports_field_errors(client, admin, origin):
seed_author(client, admin, origin)
body = sync_ok(client, admin, "posts", origin, [post_record("p1", content="short")])
result = body["results"][0]
assert result["status"] == "rejected" and result["reason"] == "invalid"
assert any(error.startswith("content:") for error in result["errors"])
def test_future_timestamps_are_clamped(client, admin, origin):
seed_author(client, admin, origin)
body = sync_ok(client, admin, "posts", origin, [post_record("p1", created_at="2999-01-01T00:00:00Z")])
row = get_table("posts").find_one(uid=body["results"][0]["local_uid"])
assert row["created_at"] < "2100"
def test_post_shows_up_on_its_detail_page(client, admin, origin):
seed_author(client, admin, origin)
body = sync_ok(client, admin, "posts", origin, [post_record("p1", title="Visible synced post")])
slug = get_table("posts").find_one(uid=body["results"][0]["local_uid"])["slug"]
page = client.get(f"/posts/{slug}")
assert page.status_code == 200 and "Visible synced post" in page.text
+39
View File
@@ -0,0 +1,39 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_setting, set_setting
from tests.api.sync.conftest import sync_ok, unique, user_record
def test_admin_settings_save_normalizes_sync_origins(client, admin):
previous = get_setting("sync_origins", "")
try:
response = client.post(
"/admin/settings",
json={"sync_origins": "A.example.org, b.example.org\nA.example.org"},
headers=admin["headers"],
)
assert response.status_code == 200, response.text
set_setting_cache_free = get_setting("sync_origins", "")
assert set_setting_cache_free == "a.example.org,b.example.org"
refused = client.post(
"/admin/settings", json={"sync_origins": "bad origin!"}, headers=admin["headers"]
)
assert refused.status_code in (400, 422)
finally:
set_setting("sync_origins", previous)
def test_admin_settings_page_shows_sync_controls(client, admin):
page = client.get("/admin/settings", headers={"X-API-KEY": admin["headers"]["X-API-KEY"]})
assert page.status_code == 200
assert 'name="sync_enabled"' in page.text and 'name="sync_origins"' in page.text
def test_clearing_sync_origins_removes_the_allowlist(client, admin, origin):
set_setting("sync_origins", "only.example.org")
try:
client.post("/admin/settings", json={"sync_origins": ""}, headers=admin["headers"])
assert get_setting("sync_origins", "x") == ""
sync_ok(client, admin, "users", origin, [user_record("u1", unique("any"))])
finally:
set_setting("sync_origins", "")
+15
View File
@@ -0,0 +1,15 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.sync.spec import SPEC
def test_spec_is_public_and_matches_code(client):
response = client.get("/sync/spec.json")
assert response.status_code == 200
body = response.json()
assert body["kinds"] == list(SPEC.kinds)
assert body["statuses"] == list(SPEC.statuses)
assert body["limits"]["max_batch_records"] == SPEC.limits.max_batch_records
assert set(body["records"]) == set(SPEC.kinds)
assert "username" in body["records"]["users"]["properties"]
assert "stars" not in body["records"]["posts"]["properties"]
+173
View File
@@ -0,0 +1,173 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_table, set_setting
from tests.api.sync.conftest import make_account, sync, sync_ok, unique, user_record
def test_requires_authentication(client, origin):
response = client.post(
"/sync/users", json={"origin": origin, "records": [user_record()]}
)
assert response.status_code == 401
def test_member_is_refused(client, origin):
member = make_account()
response = sync(client, member, "users", origin, [user_record()])
assert response.status_code == 403
def test_kill_switch_refuses_when_disabled(client, admin, origin):
set_setting("sync_enabled", "0")
try:
response = sync(client, admin, "users", origin, [user_record()])
assert response.status_code == 503
finally:
set_setting("sync_enabled", "1")
def test_origin_allowlist(client, admin, origin):
set_setting("sync_origins", "trusted.example.org")
try:
assert sync(client, admin, "users", origin, [user_record()]).status_code == 403
allowed = sync(client, admin, "users", "trusted.example.org", [user_record(unique("a"), unique("allow"))])
assert allowed.status_code == 200
finally:
set_setting("sync_origins", "")
def test_invalid_envelope_is_422(client, admin, origin):
assert sync(client, admin, "users", origin, []).status_code == 422
assert sync(client, admin, "users", "bad origin!", [user_record()]).status_code == 422
def test_create_then_replay_is_idempotent(client, admin, origin):
name = unique("bob")
first = sync_ok(client, admin, "users", origin, [user_record("u1", name, bio="hi")])
result = first["results"][0]
assert result["status"] == "created" and result["local_uid"]
again = sync_ok(client, admin, "users", origin, [user_record("u1", name, bio="hi")])
assert again["results"][0]["status"] == "unchanged"
assert again["results"][0]["local_uid"] == result["local_uid"]
assert get_table("users").count(username=name) == 1
def test_update_changes_profile_and_keeps_local_uid(client, admin, origin):
name = unique("upd")
created = sync_ok(client, admin, "users", origin, [user_record("u1", name)])
uid = created["results"][0]["local_uid"]
updated = sync_ok(
client,
admin,
"users",
origin,
[user_record("u1", name, bio="new bio", updated_at="2025-06-01T00:00:00Z")],
)
assert updated["results"][0]["status"] == "updated"
assert get_table("users").find_one(uid=uid)["bio"] == "new bio"
def test_older_update_is_stale(client, admin, origin):
name = unique("stale")
sync_ok(client, admin, "users", origin, [user_record("u1", name, updated_at="2025-06-01T00:00:00Z")])
older = sync_ok(
client, admin, "users", origin,
[user_record("u1", name, bio="old", updated_at="2025-02-01T00:00:00Z")],
)
assert older["results"][0]["status"] == "stale"
def test_username_is_normalized_and_collisions_get_a_suffix(client, admin, origin):
taken = unique("taken")
make_account_row = get_table("users")
sync_ok(client, admin, "users", origin, [user_record("u0", taken)])
other = sync_ok(client, admin, "users", origin, [user_record("u1", taken)])
local = make_account_row.find_one(uid=other["results"][0]["local_uid"])
assert local["username"] != taken and local["username"].startswith(taken[:10])
odd = sync_ok(client, admin, "users", origin, [user_record("u2", "Ünï cøde!")])
created = make_account_row.find_one(uid=odd["results"][0]["local_uid"])
assert created["username"].replace("_", "").replace("-", "").isalnum()
def test_counters_credentials_and_role_are_ignored_and_reported(client, admin, origin):
body = sync_ok(
client, admin, "users", origin,
[user_record("u1", unique("priv"), xp=999, level=50, stars=7, role="Admin", email="x@y.z", password_hash="p")],
)
result = body["results"][0]
assert set(result["ignored"]) == {"xp", "level", "stars", "role", "email", "password_hash"}
row = get_table("users").find_one(uid=result["local_uid"])
assert row["role"] == "Member" and row["xp"] == 0 and row["level"] == 1
assert row["email"].endswith("@sync.invalid")
def test_synced_account_cannot_log_in(client, admin, origin):
body = sync_ok(client, admin, "users", origin, [user_record("u1", unique("nologin"))])
row = get_table("users").find_one(uid=body["results"][0]["local_uid"])
response = client.post(
"/auth/login",
json={"email": row["email"], "password": "anything1"},
headers={"Accept": "application/json"},
)
assert response.status_code == 401
def test_dry_run_writes_nothing(client, admin, origin):
name = unique("dry")
body = sync_ok(client, admin, "users", origin, [user_record("u1", name)], dry_run=True)
assert body["dry_run"] is True and body["results"][0]["status"] == "created"
assert get_table("users").count(username=name) == 0
assert get_table("sync_links").count(origin=origin) == 0
def test_one_bad_record_does_not_fail_the_batch(client, admin, origin):
body = sync_ok(
client, admin, "users", origin,
[user_record("u1", unique("good")), {"origin_uid": "", "username": "x"}, user_record("u3", unique("good"), created_at="nonsense")],
)
assert [r["status"] for r in body["results"]] == ["created", "rejected", "rejected"]
assert body["counts"]["created"] == 1 and body["counts"]["rejected"] == 2
assert all(r["errors"] for r in body["results"][1:])
def test_deleted_flag_deactivates_account(client, admin, origin):
name = unique("gone")
created = sync_ok(client, admin, "users", origin, [user_record("u1", name)])
sync_ok(client, admin, "users", origin, [user_record("u1", name, deleted=True, updated_at="2025-05-01T00:00:00Z")])
assert not get_table("users").find_one(uid=created["results"][0]["local_uid"])["is_active"]
def test_tombstone_for_unknown_record_creates_nothing(client, admin, origin):
body = sync_ok(client, admin, "users", origin, [user_record("u1", unique("ghost"), deleted=True)])
assert body["results"][0]["status"] == "unchanged"
assert body["results"][0]["reason"] == "tombstone_for_unknown"
assert get_table("sync_links").count(origin=origin) == 0
def test_same_origin_uid_in_different_origins_are_distinct(client, admin, origin):
first = sync_ok(client, admin, "users", origin, [user_record("u1", unique("one"))])
second = sync_ok(client, admin, "users", f"b.{origin}", [user_record("u1", unique("two"))])
assert first["results"][0]["local_uid"] != second["results"][0]["local_uid"]
def test_form_encoded_body_with_json_records_is_accepted(client, admin, origin):
import json
name = unique("form")
response = client.post(
"/sync/users",
data={"origin": origin, "records": json.dumps([user_record("u1", name)]), "dry_run": "true"},
headers=admin["headers"],
)
assert response.status_code == 200, response.text
assert response.json()["results"][0]["status"] == "created"
assert get_table("users").count(username=name) == 0
def test_malformed_records_string_is_422(client, admin, origin):
response = client.post(
"/sync/users",
data={"origin": origin, "records": "not json"},
headers=admin["headers"],
)
assert response.status_code in (400, 422)