# retoor <retoor@molodetz.nl>
import io
import uuid
import pytest
import requests
from PIL import Image
from tests.conftest import BASE_URL
from devplacepy.database import get_table, refresh_snapshot, set_setting
JSON_manage = {"Accept": "application/json"}
@pytest.fixture(scope="module", autouse=True)
def _manage_settings(app_server):
for key, value in {
"rate_limit_per_minute": "1000000",
"registration_open": "1",
"maintenance_mode": "0",
"max_upload_size_mb": "10",
"allowed_file_types": "",
}.items():
set_setting(key, value)
yield
def _db_user(name):
refresh_snapshot()
return get_table("users").find_one(username=name)
def _user_manage(prefix="mng"):
s = requests.Session()
name = f"{prefix}_{uuid.uuid4().hex[:10]}"
s.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@test.dev",
"password": "secret123",
"confirm_password": "secret123",
},
allow_redirects=True,
)
return s, name
def _png_bytes():
buf = io.BytesIO()
Image.new("RGB", (4, 4), (0, 128, 255)).save(buf, "PNG")
return buf.getvalue()
def _upload(s, name="a.png", content=None, content_type="image/png"):
r = s.post(
f"{BASE_URL}/uploads/upload",
files={"file": (name, content or _png_bytes(), content_type)},
)
assert r.status_code == 201, r.text
return r.json()["uid"]
def _admin_session(seeded_db):
key = _db_user("alice_test")["api_key"]
s = requests.Session()
s.headers.update({"X-API-KEY": key})
return s
def test_list_requires_login(app_server):
r = requests.get(f"{BASE_URL}/uploads", headers=JSON_manage, allow_redirects=False)
assert r.status_code == 401
def test_list_own_attachments_newest_first(app_server):
s, _ = _user_manage()
first = _upload(s, "first.png")
second = _upload(s, "second.txt", b"hello world", "text/plain")
r = s.get(f"{BASE_URL}/uploads", headers=JSON_manage)
assert r.status_code == 200, r.text
body = r.json()
uids = [a["uid"] for a in body["attachments"]]
assert first in uids and second in uids
assert uids.index(second) < uids.index(first)
assert body["total"] >= 2
assert body["pagination"]["page"] == 1
def test_list_is_isolated_per_user(app_server):
alice, _ = _user_manage()
uid = _upload(alice, "alice.png")
bob, _ = _user_manage()
r = bob.get(f"{BASE_URL}/uploads", headers=JSON_manage)
assert r.status_code == 200
assert uid not in [a["uid"] for a in r.json()["attachments"]]
def test_list_linked_filter(app_server):
s, _ = _user_manage()
orphan = _upload(s, "orphan.png")
attached = _upload(s, "used.png")
post = s.post(
f"{BASE_URL}/posts/create",
headers=JSON_manage,
data={
"title": f"manage {uuid.uuid4().hex[:6]}",
"content": "a post that carries an attachment",
"topic": "devlog",
"attachment_uids": attached,
},
)
assert post.status_code in (200, 201), post.text
linked = s.get(f"{BASE_URL}/uploads", headers=JSON_manage, params={"linked": "true"})
linked_uids = [a["uid"] for a in linked.json()["attachments"]]
assert attached in linked_uids
assert orphan not in linked_uids
unlinked = s.get(
f"{BASE_URL}/uploads", headers=JSON_manage, params={"linked": "false"}
)
unlinked_uids = [a["uid"] for a in unlinked.json()["attachments"]]
assert orphan in unlinked_uids
assert attached not in unlinked_uids
attached_item = next(
a for a in linked.json()["attachments"] if a["uid"] == attached
)
assert attached_item["linked"] is True
assert attached_item["target_type"] == "post"
assert attached_item["target_url"]
def test_get_one_attachment(app_server):
s, _ = _user_manage()
uid = _upload(s, "single.png")
r = s.get(f"{BASE_URL}/uploads/{uid}", headers=JSON_manage)
assert r.status_code == 200, r.text
body = r.json()
assert body["uid"] == uid
assert body["original_filename"] == "single.png"
assert body["is_image"] is True
def test_get_missing_attachment_404(app_server):
s, _ = _user_manage()
r = s.get(f"{BASE_URL}/uploads/{uuid.uuid4().hex}", headers=JSON_manage)
assert r.status_code == 404
def test_get_other_users_attachment_forbidden(app_server):
alice, _ = _user_manage()
uid = _upload(alice, "private.png")
bob, _ = _user_manage()
r = bob.get(f"{BASE_URL}/uploads/{uid}", headers=JSON_manage)
assert r.status_code == 403
def test_admin_can_get_any_attachment(seeded_db):
member, _ = _user_manage()
uid = _upload(member, "member.png")
admin = _admin_session(seeded_db)
r = admin.get(f"{BASE_URL}/uploads/{uid}", headers=JSON_manage)
assert r.status_code == 200
assert r.json()["uid"] == uid
def test_rename_attachment(app_server):
s, _ = _user_manage()
uid = _upload(s, "before.png")
r = s.patch(
f"{BASE_URL}/uploads/{uid}", headers=JSON_manage, data={"filename": "after"}
)
assert r.status_code == 200, r.text
assert r.json()["original_filename"] == "after.png"
check = s.get(f"{BASE_URL}/uploads/{uid}", headers=JSON_manage)
assert check.json()["original_filename"] == "after.png"
def test_rename_preserves_extension(app_server):
s, _ = _user_manage()
uid = _upload(s, "safe.png")
r = s.patch(
f"{BASE_URL}/uploads/{uid}", headers=JSON_manage, data={"filename": "evil.html"}
)
assert r.status_code == 200, r.text
assert r.json()["original_filename"] == "evil.png"
def test_rename_other_users_attachment_forbidden(app_server):
alice, _ = _user_manage()
uid = _upload(alice, "keep.png")
bob, _ = _user_manage()
r = bob.patch(
f"{BASE_URL}/uploads/{uid}", headers=JSON_manage, data={"filename": "hijack"}
)
assert r.status_code == 403
def test_rename_missing_attachment_404(app_server):
s, _ = _user_manage()
r = s.patch(
f"{BASE_URL}/uploads/{uuid.uuid4().hex}",
headers=JSON_manage,
data={"filename": "x"},
)
assert r.status_code == 404
def test_rename_recorded(seeded_db):
s, _ = _user_manage()
uid = _upload(s, "audited.png")
s.patch(
f"{BASE_URL}/uploads/{uid}", headers=JSON_manage, data={"filename": "renamed"}
)
admin = _admin_session(seeded_db)
log = admin.get(
f"{BASE_URL}/admin/audit-log",
headers=JSON_manage,
params={"event_key": "attachment.rename"},
)
assert log.status_code == 200
assert any(e.get("target_uid") == uid for e in log.json()["entries"])