174 lines
7.2 KiB
Python
174 lines
7.2 KiB
Python
# 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)
|