2026-05-23 03:21:55 +02:00
|
|
|
import io
|
2026-05-27 21:06:18 +02:00
|
|
|
import re
|
|
|
|
|
import uuid
|
2026-05-23 03:21:55 +02:00
|
|
|
import requests
|
|
|
|
|
from PIL import Image
|
|
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
|
|
|
|
|
|
2026-05-27 21:06:18 +02:00
|
|
|
def _user(prefix="up"):
|
2026-05-23 03:21:55 +02:00
|
|
|
s = requests.Session()
|
2026-05-27 21:06:18 +02:00
|
|
|
name = f"{prefix}_{uuid.uuid4().hex[:10]}"
|
2026-05-23 03:21:55 +02:00
|
|
|
s.post(f"{BASE_URL}/auth/signup", data={
|
|
|
|
|
"username": name,
|
|
|
|
|
"email": f"{name}@test.dev",
|
|
|
|
|
"password": "secret123",
|
|
|
|
|
"confirm_password": "secret123",
|
|
|
|
|
}, allow_redirects=True)
|
2026-05-27 21:06:18 +02:00
|
|
|
return s, name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _session():
|
|
|
|
|
s, _ = _user()
|
2026-05-23 03:21:55 +02:00
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _png_bytes():
|
|
|
|
|
buf = io.BytesIO()
|
|
|
|
|
Image.new("RGB", (4, 4), (255, 0, 0)).save(buf, "PNG")
|
|
|
|
|
return buf.getvalue()
|
|
|
|
|
|
|
|
|
|
|
2026-05-27 21:06:18 +02:00
|
|
|
def _upload(s, name="a.png"):
|
|
|
|
|
r = s.post(f"{BASE_URL}/uploads/upload", files={"file": (name, _png_bytes(), "image/png")})
|
|
|
|
|
assert r.status_code == 201, r.text
|
|
|
|
|
return r.json()["uid"]
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 03:21:55 +02:00
|
|
|
def test_upload_allowed_png(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
r = s.post(f"{BASE_URL}/uploads/upload", files={"file": ("x.png", _png_bytes(), "image/png")})
|
|
|
|
|
assert r.status_code == 201, r.text
|
|
|
|
|
assert "url" in r.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upload_rejects_svg(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
r = s.post(f"{BASE_URL}/uploads/upload", files={"file": ("x.svg", b"<svg/>", "image/svg+xml")})
|
|
|
|
|
assert r.status_code == 415
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upload_rejects_html(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
r = s.post(f"{BASE_URL}/uploads/upload", files={"file": ("x.html", b"<html></html>", "text/html")})
|
|
|
|
|
assert r.status_code == 415
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upload_rejects_oversize(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
big = b"\x89PNG\r\n" + b"\x00" * (11 * 1024 * 1024)
|
|
|
|
|
r = s.post(f"{BASE_URL}/uploads/upload", files={"file": ("big.png", big, "image/png")})
|
|
|
|
|
assert r.status_code == 413
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_uploaded_file_served_as_attachment(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
r = s.post(f"{BASE_URL}/uploads/upload", files={"file": ("x.png", _png_bytes(), "image/png")})
|
|
|
|
|
url = r.json()["url"]
|
|
|
|
|
served = s.get(f"{BASE_URL}{url}")
|
|
|
|
|
assert served.status_code == 200
|
|
|
|
|
assert served.headers.get("Content-Disposition") == "attachment"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upload_requires_login(app_server):
|
|
|
|
|
r = requests.post(f"{BASE_URL}/uploads/upload", files={"file": ("x.png", _png_bytes(), "image/png")}, allow_redirects=False)
|
2026-05-23 08:34:13 +02:00
|
|
|
assert r.status_code == 401
|
2026-05-23 03:21:55 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_own_allowed_other_user_forbidden(app_server):
|
|
|
|
|
alice = _session()
|
|
|
|
|
uid = alice.post(f"{BASE_URL}/uploads/upload", files={"file": ("x.png", _png_bytes(), "image/png")}).json()["uid"]
|
|
|
|
|
bob = _session()
|
|
|
|
|
assert bob.delete(f"{BASE_URL}/uploads/delete/{uid}").status_code == 403
|
|
|
|
|
assert alice.delete(f"{BASE_URL}/uploads/delete/{uid}").status_code == 200
|
2026-05-27 21:06:18 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_post_links_multiple_attachments(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
u1, u2 = _upload(s), _upload(s)
|
|
|
|
|
r = s.post(f"{BASE_URL}/posts/create", data={
|
|
|
|
|
"content": "Post body with two attachments here",
|
|
|
|
|
"title": "attach post", "topic": "random",
|
|
|
|
|
"attachment_uids": f"{u1},{u2}",
|
|
|
|
|
}, allow_redirects=True)
|
|
|
|
|
assert r.status_code == 200, r.text[:300]
|
|
|
|
|
assert "/posts/" in r.url, r.url
|
|
|
|
|
assert u1 in r.text and u2 in r.text, "both attachments must be linked and displayed on the post"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_comment_links_multiple_attachments(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
post = s.post(f"{BASE_URL}/posts/create", data={
|
|
|
|
|
"content": "Host post for comment attachments", "title": "host", "topic": "random",
|
|
|
|
|
}, allow_redirects=True)
|
|
|
|
|
target_uid = re.search(r'name="target_uid"\s+value="([^"]+)"', post.text).group(1)
|
|
|
|
|
|
|
|
|
|
u1, u2 = _upload(s), _upload(s)
|
|
|
|
|
r = s.post(f"{BASE_URL}/comments/create", data={
|
|
|
|
|
"content": "Comment with attachments",
|
|
|
|
|
"target_uid": target_uid, "target_type": "post",
|
|
|
|
|
"attachment_uids": f"{u1},{u2}",
|
|
|
|
|
}, allow_redirects=True)
|
|
|
|
|
assert r.status_code == 200, r.text[:300]
|
|
|
|
|
assert u1 in r.text and u2 in r.text, "both attachments must be linked and displayed on the comment"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_project_links_multiple_attachments(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
u1, u2 = _upload(s), _upload(s)
|
|
|
|
|
r = s.post(f"{BASE_URL}/projects/create", data={
|
|
|
|
|
"title": "Attach Project", "description": "Project with attachments",
|
|
|
|
|
"project_type": "software", "platforms": "linux", "status": "In Development",
|
|
|
|
|
"attachment_uids": f"{u1},{u2}",
|
|
|
|
|
}, allow_redirects=True)
|
|
|
|
|
assert r.status_code == 200, r.text[:300]
|
|
|
|
|
assert "/projects/" in r.url, r.url
|
|
|
|
|
assert u1 in r.text and u2 in r.text, "both attachments must be linked and displayed on the project"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_links_multiple_attachments(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
u1, u2 = _upload(s), _upload(s)
|
|
|
|
|
r = s.post(f"{BASE_URL}/gists/create", data={
|
|
|
|
|
"title": "Attach Gist", "description": "Gist with attachments",
|
|
|
|
|
"source_code": "print('hi')", "language": "python",
|
|
|
|
|
"attachment_uids": f"{u1},{u2}",
|
|
|
|
|
}, allow_redirects=True)
|
|
|
|
|
assert r.status_code == 200, r.text[:300]
|
|
|
|
|
assert "/gists/" in r.url, r.url
|
|
|
|
|
assert u1 in r.text and u2 in r.text, "both attachments must be linked and displayed on the gist"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bug_links_multiple_attachments(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
u1, u2 = _upload(s), _upload(s)
|
|
|
|
|
r = s.post(f"{BASE_URL}/bugs/create", data={
|
|
|
|
|
"title": "Attach Bug", "description": "Bug report with attachments",
|
|
|
|
|
"attachment_uids": f"{u1},{u2}",
|
|
|
|
|
}, allow_redirects=True)
|
|
|
|
|
assert r.status_code == 200, r.text[:300]
|
|
|
|
|
assert u1 in r.text and u2 in r.text, "both attachments must be linked and displayed on the bug"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_message_links_multiple_attachments(app_server):
|
|
|
|
|
alice = _session()
|
|
|
|
|
bob, bob_name = _user("bob")
|
|
|
|
|
found = alice.get(f"{BASE_URL}/messages/search", params={"q": bob_name}).json()["results"]
|
|
|
|
|
bob_uid = found[0]["uid"]
|
|
|
|
|
|
|
|
|
|
u1, u2 = _upload(alice), _upload(alice)
|
|
|
|
|
r = alice.post(f"{BASE_URL}/messages/send", data={
|
|
|
|
|
"content": "Message with attachments", "receiver_uid": bob_uid,
|
|
|
|
|
"attachment_uids": f"{u1},{u2}",
|
|
|
|
|
}, allow_redirects=True)
|
|
|
|
|
assert r.status_code == 200, r.text[:300]
|
|
|
|
|
assert u1 in r.text and u2 in r.text, "both attachments must be linked and displayed in the conversation"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_single_attachment_links_to_post(app_server):
|
|
|
|
|
s = _session()
|
|
|
|
|
u1 = _upload(s)
|
|
|
|
|
r = s.post(f"{BASE_URL}/posts/create", data={
|
|
|
|
|
"content": "Post body with one attachment", "title": "one", "topic": "random",
|
|
|
|
|
"attachment_uids": u1,
|
|
|
|
|
}, allow_redirects=True)
|
|
|
|
|
assert r.status_code == 200, r.text[:300]
|
|
|
|
|
assert u1 in r.text, "single attachment must be linked and displayed"
|