@@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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", "")
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user