import uuid from tests.conftest import register_and_login def _unique(prefix: str) -> str: return f"{prefix}-{uuid.uuid4().hex[:8]}" async def test_upload_file_succeeds(client, db_session): await register_and_login(client, db_session, username=_unique("alice")) room = (await client.post("/api/rooms", json={"name": _unique("general")})).json() resp = await client.post( f"/api/rooms/{room['id']}/files", files={"file": ("report.pdf", b"%PDF-1.4 not a real pdf", "application/pdf")}, ) assert resp.status_code == 201, resp.text body = resp.json() assert "id" in body assert body["filename"] == "report.pdf" assert body["size_bytes"] == len(b"%PDF-1.4 not a real pdf") assert body["content_type"] == "application/pdf" async def test_upload_any_content_type_accepted(client, db_session): await register_and_login(client, db_session, username=_unique("alice")) room = (await client.post("/api/rooms", json={"name": _unique("general")})).json() # Unlike /images, /files has no content-type allowlist -- arbitrary # files are the point of this endpoint. resp = await client.post( f"/api/rooms/{room['id']}/files", files={"file": ("archive.zip", b"PK\x03\x04 not a real zip", "application/zip")}, ) assert resp.status_code == 201, resp.text async def test_upload_oversized_file_rejected(client, db_session): await register_and_login(client, db_session, username=_unique("alice")) room = (await client.post("/api/rooms", json={"name": _unique("general")})).json() oversized = b"0" * (9 * 1024 * 1024) resp = await client.post( f"/api/rooms/{room['id']}/files", files={"file": ("huge.bin", oversized, "application/octet-stream")}, ) assert resp.status_code == 413 async def test_serve_file_requires_room_membership(client, db_session): await register_and_login(client, db_session, username=_unique("alice")) room = (await client.post("/api/rooms", json={"name": _unique("general")})).json() upload = await client.post( f"/api/rooms/{room['id']}/files", files={"file": ("notes.txt", b"hello", "text/plain")}, ) file_id = upload.json()["id"] ok = await client.get(f"/api/rooms/{room['id']}/files/{file_id}") assert ok.status_code == 200 await register_and_login(client, db_session, username=_unique("bob")) resp = await client.get(f"/api/rooms/{room['id']}/files/{file_id}") assert resp.status_code == 403 async def test_serve_file_404s_for_wrong_room(client, db_session): await register_and_login(client, db_session, username=_unique("alice")) room_a = (await client.post("/api/rooms", json={"name": _unique("room-a")})).json() room_b = (await client.post("/api/rooms", json={"name": _unique("room-b")})).json() upload = await client.post( f"/api/rooms/{room_a['id']}/files", files={"file": ("notes.txt", b"hello", "text/plain")}, ) file_id = upload.json()["id"] resp = await client.get(f"/api/rooms/{room_b['id']}/files/{file_id}") assert resp.status_code == 404 async def test_serve_file_forces_download(client, db_session): # The security-relevant assertion for this feature: a user-uploaded # file must never render inline (e.g. an uploaded .html executing # script same-origin) -- Content-Disposition: attachment forces a # download in the browser regardless of content-type. await register_and_login(client, db_session, username=_unique("alice")) room = (await client.post("/api/rooms", json={"name": _unique("general")})).json() upload = await client.post( f"/api/rooms/{room['id']}/files", files={"file": ("notes.txt", b"hello", "text/plain")}, ) file_id = upload.json()["id"] resp = await client.get(f"/api/rooms/{room['id']}/files/{file_id}") assert resp.status_code == 200 disposition = resp.headers["content-disposition"] assert "attachment" in disposition assert "notes.txt" in disposition async def test_serve_allowlisted_video_inline(client, db_session): # #65: a