chore: reorganize test files into domain-specific subdirectories under tests/
Split the monolithic test directory into three tiers (unit, api, e2e) with a path-mirroring directory structure. Added corresponding Makefile targets (test-unit, test-api, test-e2e) and updated all documentation references (CLAUDE.md, README.md, testing-cicd.html, testing-framework.html, testing-make.html) to reflect the new layout and naming conventions.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import io
|
||||
import uuid
|
||||
import requests
|
||||
from PIL import Image
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
JSON_media = {"Accept": "application/json"}
|
||||
def _png_bytes_media(color=(200, 30, 30)):
|
||||
buf = io.BytesIO()
|
||||
Image.new("RGB", (8, 8), color).save(buf, "PNG")
|
||||
return buf.getvalue()
|
||||
def _signup_media(prefix="media"):
|
||||
s = requests.Session()
|
||||
name = f"{prefix}_{uuid.uuid4().hex[:10]}"
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/signup",
|
||||
data={
|
||||
"username": name,
|
||||
"email": f"{name}@test.devplace",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s, name
|
||||
def _login_media(seeded_db, who):
|
||||
s = requests.Session()
|
||||
creds = seeded_db[who]
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={"email": creds["email"], "password": creds["password"]},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s
|
||||
def _upload_media(s, name="pic.png", content=None, mime="image/png"):
|
||||
files = {"file": (name, content if content is not None else _png_bytes_media(), mime)}
|
||||
r = s.post(f"{BASE_URL}/uploads/upload", files=files)
|
||||
assert r.status_code == 201, r.text
|
||||
return r.json()["uid"]
|
||||
def _create_post_media(s, attachment_uids, title="media post"):
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
data={
|
||||
"content": "A post that carries media for the gallery tests.",
|
||||
"title": title,
|
||||
"topic": "random",
|
||||
"attachment_uids": attachment_uids,
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
assert "/posts/" in r.url, r.url
|
||||
return r.url
|
||||
def _create_project_media(s, attachment_uids, title="Media Project"):
|
||||
r = s.post(
|
||||
f"{BASE_URL}/projects/create",
|
||||
data={
|
||||
"title": title,
|
||||
"description": "Project with media for gallery tests.",
|
||||
"project_type": "software",
|
||||
"platforms": "linux",
|
||||
"status": "In Development",
|
||||
"attachment_uids": attachment_uids,
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
assert "/projects/" in r.url, r.url
|
||||
return r.url
|
||||
def _media_json(session, username):
|
||||
r = session.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
return r.json()
|
||||
def _media_uids(session, username):
|
||||
return [m["uid"] for m in _media_json(session, username)["media"]]
|
||||
def _seed_media_via_browser(page, title="ui media"):
|
||||
up = page.request.post(
|
||||
f"{BASE_URL}/uploads/upload",
|
||||
multipart={
|
||||
"file": {
|
||||
"name": "pic.png",
|
||||
"mimeType": "image/png",
|
||||
"buffer": _png_bytes_media(),
|
||||
}
|
||||
},
|
||||
)
|
||||
assert up.ok, up.text()
|
||||
uid = up.json()["uid"]
|
||||
post = page.request.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
form={
|
||||
"content": "media for the ui gallery test",
|
||||
"title": title,
|
||||
"topic": "random",
|
||||
"attachment_uids": uid,
|
||||
},
|
||||
)
|
||||
assert post.ok, post.text()
|
||||
return uid
|
||||
def _media_uids_via_page(page, username):
|
||||
resp = page.request.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
|
||||
return [m["uid"] for m in resp.json()["media"]]
|
||||
|
||||
|
||||
def test_delete_requires_login(app_server):
|
||||
owner, name = _signup_media()
|
||||
uid = _upload_media(owner)
|
||||
_create_post_media(owner, uid)
|
||||
anon = requests.Session()
|
||||
r = anon.post(f"{BASE_URL}/media/{uid}/delete", headers=JSON_media)
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_delete_nonexistent_media(app_server):
|
||||
s, _ = _signup_media()
|
||||
r = s.post(f"{BASE_URL}/media/nonexistent-uid/delete", headers=JSON_media)
|
||||
assert r.status_code in (302, 303, 404)
|
||||
|
||||
|
||||
def test_member_cannot_delete_other_members_media(app_server):
|
||||
owner, owner_name = _signup_media("owner")
|
||||
uid = _upload_media(owner)
|
||||
_create_post_media(owner, uid)
|
||||
attacker, _ = _signup_media("attacker")
|
||||
r = attacker.post(f"{BASE_URL}/media/{uid}/delete", headers=JSON_media)
|
||||
assert r.status_code == 403
|
||||
# still visible to the owner
|
||||
assert uid in _media_uids(owner, owner_name)
|
||||
|
||||
|
||||
def test_owner_soft_deletes_own_media(app_server):
|
||||
owner, name = _signup_media()
|
||||
uid = _upload_media(owner)
|
||||
_create_post_media(owner, uid)
|
||||
assert uid in _media_uids(owner, name)
|
||||
|
||||
r = owner.post(f"{BASE_URL}/media/{uid}/delete", headers=JSON_media)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["ok"] is True
|
||||
|
||||
# gone from the gallery (parent-object hiding is asserted in
|
||||
# test_soft_delete_hides_attachment_from_feed)
|
||||
assert uid not in _media_uids(owner, name)
|
||||
|
||||
|
||||
def test_soft_delete_hides_attachment_from_feed(app_server):
|
||||
# The parent-object invariant: a soft-deleted attachment disappears from the place it
|
||||
# was attached. The feed reads attachments via the raw-query batch helper, so this is
|
||||
# a reliable end-to-end check that the relation is hidden (not unlinked).
|
||||
s, name = _signup_media()
|
||||
uid = _upload_media(s)
|
||||
_create_post_media(s, uid, title="feed invariant post")
|
||||
|
||||
def attached_on_feed():
|
||||
posts = s.get(f"{BASE_URL}/feed", headers=JSON_media).json().get("posts", [])
|
||||
for item in posts:
|
||||
uids = [a.get("uid") for a in (item.get("attachments") or [])]
|
||||
if uid in uids:
|
||||
return True
|
||||
return False
|
||||
|
||||
assert attached_on_feed()
|
||||
r = s.post(f"{BASE_URL}/media/{uid}/delete", headers=JSON_media)
|
||||
assert r.status_code == 200
|
||||
assert not attached_on_feed()
|
||||
|
||||
|
||||
def test_admin_can_delete_any_media(seeded_db):
|
||||
owner, owner_name = _signup_media("victim")
|
||||
uid = _upload_media(owner)
|
||||
_create_post_media(owner, uid)
|
||||
admin = _login_media(seeded_db, "alice")
|
||||
r = admin.post(f"{BASE_URL}/media/{uid}/delete", headers=JSON_media)
|
||||
assert r.status_code == 200
|
||||
assert uid not in _media_uids(owner, owner_name)
|
||||
|
||||
|
||||
def test_admin_trash_lists_deleted_media(seeded_db):
|
||||
owner, owner_name = _signup_media("trashed")
|
||||
uid = _upload_media(owner)
|
||||
_create_post_media(owner, uid)
|
||||
owner.post(f"{BASE_URL}/media/{uid}/delete", headers=JSON_media)
|
||||
|
||||
admin = _login_media(seeded_db, "alice")
|
||||
data = admin.get(f"{BASE_URL}/admin/media", headers=JSON_media).json()
|
||||
item = next((m for m in data["media"] if m["uid"] == uid), None)
|
||||
assert item is not None
|
||||
assert item["uploader"] == owner_name
|
||||
assert item["deleted_at"]
|
||||
@@ -0,0 +1,164 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import io
|
||||
import uuid
|
||||
import requests
|
||||
from PIL import Image
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
JSON_media = {"Accept": "application/json"}
|
||||
def _png_bytes_media(color=(200, 30, 30)):
|
||||
buf = io.BytesIO()
|
||||
Image.new("RGB", (8, 8), color).save(buf, "PNG")
|
||||
return buf.getvalue()
|
||||
def _signup_media(prefix="media"):
|
||||
s = requests.Session()
|
||||
name = f"{prefix}_{uuid.uuid4().hex[:10]}"
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/signup",
|
||||
data={
|
||||
"username": name,
|
||||
"email": f"{name}@test.devplace",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s, name
|
||||
def _login_media(seeded_db, who):
|
||||
s = requests.Session()
|
||||
creds = seeded_db[who]
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={"email": creds["email"], "password": creds["password"]},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s
|
||||
def _upload_media(s, name="pic.png", content=None, mime="image/png"):
|
||||
files = {"file": (name, content if content is not None else _png_bytes_media(), mime)}
|
||||
r = s.post(f"{BASE_URL}/uploads/upload", files=files)
|
||||
assert r.status_code == 201, r.text
|
||||
return r.json()["uid"]
|
||||
def _create_post_media(s, attachment_uids, title="media post"):
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
data={
|
||||
"content": "A post that carries media for the gallery tests.",
|
||||
"title": title,
|
||||
"topic": "random",
|
||||
"attachment_uids": attachment_uids,
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
assert "/posts/" in r.url, r.url
|
||||
return r.url
|
||||
def _create_project_media(s, attachment_uids, title="Media Project"):
|
||||
r = s.post(
|
||||
f"{BASE_URL}/projects/create",
|
||||
data={
|
||||
"title": title,
|
||||
"description": "Project with media for gallery tests.",
|
||||
"project_type": "software",
|
||||
"platforms": "linux",
|
||||
"status": "In Development",
|
||||
"attachment_uids": attachment_uids,
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
assert "/projects/" in r.url, r.url
|
||||
return r.url
|
||||
def _media_json(session, username):
|
||||
r = session.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
return r.json()
|
||||
def _media_uids(session, username):
|
||||
return [m["uid"] for m in _media_json(session, username)["media"]]
|
||||
def _seed_media_via_browser(page, title="ui media"):
|
||||
up = page.request.post(
|
||||
f"{BASE_URL}/uploads/upload",
|
||||
multipart={
|
||||
"file": {
|
||||
"name": "pic.png",
|
||||
"mimeType": "image/png",
|
||||
"buffer": _png_bytes_media(),
|
||||
}
|
||||
},
|
||||
)
|
||||
assert up.ok, up.text()
|
||||
uid = up.json()["uid"]
|
||||
post = page.request.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
form={
|
||||
"content": "media for the ui gallery test",
|
||||
"title": title,
|
||||
"topic": "random",
|
||||
"attachment_uids": uid,
|
||||
},
|
||||
)
|
||||
assert post.ok, post.text()
|
||||
return uid
|
||||
def _media_uids_via_page(page, username):
|
||||
resp = page.request.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
|
||||
return [m["uid"] for m in resp.json()["media"]]
|
||||
|
||||
|
||||
def test_media_tab_lists_linked_attachment(app_server):
|
||||
s, name = _signup_media()
|
||||
uid = _upload_media(s)
|
||||
_create_post_media(s, uid)
|
||||
data = _media_json(s, name)
|
||||
uids = [m["uid"] for m in data["media"]]
|
||||
assert uid in uids
|
||||
item = next(m for m in data["media"] if m["uid"] == uid)
|
||||
assert item["is_image"] is True
|
||||
assert item["target_type"] == "post"
|
||||
assert item["target_url"].startswith("/posts/")
|
||||
|
||||
|
||||
def test_media_tab_is_public(app_server):
|
||||
owner, name = _signup_media()
|
||||
uid = _upload_media(owner)
|
||||
_create_post_media(owner, uid)
|
||||
# an anonymous visitor can browse the gallery
|
||||
anon = requests.Session()
|
||||
assert uid in _media_uids(anon, name)
|
||||
|
||||
|
||||
def test_orphan_upload_not_listed(app_server):
|
||||
s, name = _signup_media()
|
||||
uid = _upload_media(s) # uploaded but never linked to any content
|
||||
assert uid not in _media_uids(s, name)
|
||||
|
||||
|
||||
def test_media_newest_first(app_server):
|
||||
s, name = _signup_media()
|
||||
first = _upload_media(s)
|
||||
_create_post_media(s, first, title="older")
|
||||
second = _upload_media(s)
|
||||
_create_post_media(s, second, title="newer")
|
||||
uids = _media_uids(s, name)
|
||||
assert uids.index(second) < uids.index(first)
|
||||
|
||||
|
||||
def test_media_covers_multiple_target_types(app_server):
|
||||
s, name = _signup_media()
|
||||
post_att = _upload_media(s)
|
||||
_create_post_media(s, post_att)
|
||||
project_att = _upload_media(s)
|
||||
_create_project_media(s, project_att)
|
||||
items = {m["uid"]: m for m in _media_json(s, name)["media"]}
|
||||
assert items[post_att]["target_type"] == "post"
|
||||
assert items[project_att]["target_type"] == "project"
|
||||
|
||||
|
||||
def test_media_json_omits_deleted_at(app_server):
|
||||
# Members must never be shown the soft-delete column.
|
||||
s, name = _signup_media()
|
||||
uid = _upload_media(s)
|
||||
_create_post_media(s, uid)
|
||||
data = _media_json(s, name)
|
||||
assert data["media"], "expected at least one media item"
|
||||
for item in data["media"]:
|
||||
assert "deleted_at" not in item
|
||||
@@ -0,0 +1,134 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import io
|
||||
import uuid
|
||||
import requests
|
||||
from PIL import Image
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
JSON_media = {"Accept": "application/json"}
|
||||
def _png_bytes_media(color=(200, 30, 30)):
|
||||
buf = io.BytesIO()
|
||||
Image.new("RGB", (8, 8), color).save(buf, "PNG")
|
||||
return buf.getvalue()
|
||||
def _signup_media(prefix="media"):
|
||||
s = requests.Session()
|
||||
name = f"{prefix}_{uuid.uuid4().hex[:10]}"
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/signup",
|
||||
data={
|
||||
"username": name,
|
||||
"email": f"{name}@test.devplace",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s, name
|
||||
def _login_media(seeded_db, who):
|
||||
s = requests.Session()
|
||||
creds = seeded_db[who]
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={"email": creds["email"], "password": creds["password"]},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s
|
||||
def _upload_media(s, name="pic.png", content=None, mime="image/png"):
|
||||
files = {"file": (name, content if content is not None else _png_bytes_media(), mime)}
|
||||
r = s.post(f"{BASE_URL}/uploads/upload", files=files)
|
||||
assert r.status_code == 201, r.text
|
||||
return r.json()["uid"]
|
||||
def _create_post_media(s, attachment_uids, title="media post"):
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
data={
|
||||
"content": "A post that carries media for the gallery tests.",
|
||||
"title": title,
|
||||
"topic": "random",
|
||||
"attachment_uids": attachment_uids,
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
assert "/posts/" in r.url, r.url
|
||||
return r.url
|
||||
def _create_project_media(s, attachment_uids, title="Media Project"):
|
||||
r = s.post(
|
||||
f"{BASE_URL}/projects/create",
|
||||
data={
|
||||
"title": title,
|
||||
"description": "Project with media for gallery tests.",
|
||||
"project_type": "software",
|
||||
"platforms": "linux",
|
||||
"status": "In Development",
|
||||
"attachment_uids": attachment_uids,
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
assert "/projects/" in r.url, r.url
|
||||
return r.url
|
||||
def _media_json(session, username):
|
||||
r = session.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
return r.json()
|
||||
def _media_uids(session, username):
|
||||
return [m["uid"] for m in _media_json(session, username)["media"]]
|
||||
def _seed_media_via_browser(page, title="ui media"):
|
||||
up = page.request.post(
|
||||
f"{BASE_URL}/uploads/upload",
|
||||
multipart={
|
||||
"file": {
|
||||
"name": "pic.png",
|
||||
"mimeType": "image/png",
|
||||
"buffer": _png_bytes_media(),
|
||||
}
|
||||
},
|
||||
)
|
||||
assert up.ok, up.text()
|
||||
uid = up.json()["uid"]
|
||||
post = page.request.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
form={
|
||||
"content": "media for the ui gallery test",
|
||||
"title": title,
|
||||
"topic": "random",
|
||||
"attachment_uids": uid,
|
||||
},
|
||||
)
|
||||
assert post.ok, post.text()
|
||||
return uid
|
||||
def _media_uids_via_page(page, username):
|
||||
resp = page.request.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
|
||||
return [m["uid"] for m in resp.json()["media"]]
|
||||
|
||||
|
||||
def test_restore_requires_admin(seeded_db):
|
||||
owner, name = _signup_media()
|
||||
uid = _upload_media(owner)
|
||||
_create_post_media(owner, uid)
|
||||
owner.post(f"{BASE_URL}/media/{uid}/delete", headers=JSON_media)
|
||||
# a plain member may not restore
|
||||
member = _login_media(seeded_db, "bob")
|
||||
r = member.post(f"{BASE_URL}/media/{uid}/restore", headers=JSON_media)
|
||||
assert r.status_code in (302, 303, 403)
|
||||
assert uid not in _media_uids(owner, name)
|
||||
|
||||
|
||||
def test_admin_restore_brings_media_back(seeded_db):
|
||||
owner, name = _signup_media()
|
||||
uid = _upload_media(owner)
|
||||
post_url = _create_post_media(owner, uid)
|
||||
owner.post(f"{BASE_URL}/media/{uid}/delete", headers=JSON_media)
|
||||
assert uid not in _media_uids(owner, name)
|
||||
|
||||
admin = _login_media(seeded_db, "alice")
|
||||
r = admin.post(f"{BASE_URL}/media/{uid}/restore", headers=JSON_media)
|
||||
assert r.status_code == 200
|
||||
# reappears in the gallery, and its relation to the parent post is intact
|
||||
# (delete never cleared target_type/target_uid), so it is back on the post too
|
||||
restored = {m["uid"]: m for m in _media_json(owner, name)["media"]}
|
||||
assert uid in restored
|
||||
assert restored[uid]["target_type"] == "post"
|
||||
assert post_url.endswith(restored[uid]["target_url"])
|
||||
Reference in New Issue
Block a user