Files
devplacepy/tests/api/sync/attachments.py
T

81 lines
4.1 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
import base64
from pathlib import Path
from devplacepy.config import ATTACHMENTS_DIR
from devplacepy.database import get_table
from tests.api.sync.conftest import PNG_1X1, attachment_record, seed_world, sync_ok
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_an_attachment_is_stored_and_linked_to_its_target(client, admin, origin):
seed_world(client, admin, origin)
result = sync_ok(client, admin, "attachments", origin, [attachment_record()])["results"][0]
assert result["status"] == "created"
row = get_table("attachments").find_one(uid=result["local_uid"])
assert (row["target_type"], row["target_uid"]) == ("post", local(origin, "posts", "p1"))
assert row["user_uid"] == local(origin, "users", "u1") and row["mime_type"] == "image/png"
assert (Path(ATTACHMENTS_DIR) / row["directory"] / row["stored_name"]).read_bytes() == base64.b64decode(PNG_1X1)
assert row["created_at"].startswith("2025-01-05")
def test_replay_is_unchanged_and_a_content_change_is_refused(client, admin, origin):
seed_world(client, admin, origin)
sync_ok(client, admin, "attachments", origin, [attachment_record()])
assert sync_ok(client, admin, "attachments", origin, [attachment_record()])["results"][0]["status"] == "unchanged"
other = base64.b64encode(b"GIF89a" + b"x" * 40).decode()
changed = sync_ok(client, admin, "attachments", origin, [attachment_record(content=other, filename="b.gif", updated_at="2025-02-01T00:00:00Z")])["results"][0]
assert changed["status"] == "rejected" and changed["reason"] == "immutable"
def test_tombstone_soft_deletes_and_restores(client, admin, origin):
seed_world(client, admin, origin)
created = sync_ok(client, admin, "attachments", origin, [attachment_record()])["results"][0]
gone = attachment_record(updated_at="2025-02-01T00:00:00Z", deleted=True)
assert sync_ok(client, admin, "attachments", origin, [gone])["results"][0]["status"] == "updated"
assert get_table("attachments").find_one(uid=created["local_uid"])["deleted_by"] == "sync"
back = attachment_record(updated_at="2025-03-01T00:00:00Z")
sync_ok(client, admin, "attachments", origin, [back])
assert get_table("attachments").find_one(uid=created["local_uid"])["deleted_at"] is None
def test_validation_and_type_rules(client, admin, origin):
seed_world(client, admin, origin)
body = sync_ok(
client, admin, "attachments", origin,
[
attachment_record("a1", content="not base64!!"),
attachment_record("a2", content=""),
attachment_record("a3", filename="run.exe", content=base64.b64encode(b"MZ" + b"0" * 10).decode()),
attachment_record("a4", filename="../../etc/passwd.png"),
],
)
statuses = [item["status"] for item in body["results"]]
assert statuses[:3] == ["rejected", "rejected", "rejected"]
stored = get_table("attachments").find_one(uid=body["results"][3].get("local_uid") or "x")
assert statuses[3] == "created" and stored["original_filename"] == "passwd.png"
def test_an_oversize_file_is_refused(client, admin, origin):
from devplacepy.services.sync.spec import LIMITS
seed_world(client, admin, origin)
big = base64.b64encode(b"\x00" * (LIMITS.max_attachment_bytes + 1)).decode()
result = sync_ok(client, admin, "attachments", origin, [attachment_record(content=big)])["results"][0]
assert result["status"] == "rejected" and "exceeds" in result["errors"][0]
def test_an_unknown_target_waits_and_is_released_when_the_target_arrives(client, admin, origin):
from tests.api.sync.conftest import post_record, user_record
sync_ok(client, admin, "users", origin, [user_record("u1", "attuser1")])
waiting = sync_ok(client, admin, "attachments", origin, [attachment_record(target="p1")])["results"][0]
assert waiting["status"] == "deferred"
body = sync_ok(client, admin, "posts", origin, [post_record("p1", "u1")])
assert body["released"] == {"created": 1}
assert get_table("attachments").count(target_uid=local(origin, "posts", "p1")) == 1