import io import hashlib import json from pathlib import Path import subprocess import sys import os import random import zipfile import pytest from PIL import Image sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from app import create_app TOKEN = "t" * 48 AUTH = {"Authorization": "Bearer " + TOKEN} @pytest.fixture def service(tmp_path): app = create_app(tmp_path / "data", TOKEN) app.config["TESTING"] = True return app, app.test_client(), tmp_path / "data" def photo(color="red", format="JPEG", size=(40, 20), exif=None): output = io.BytesIO() Image.new("RGB", size, color).save(output, format, **({"exif": exif} if exif else {})) return output.getvalue() def upload(client, images=None, caption="", headers=AUTH, highlight=None): return client.post("/api/photos", headers=headers, data={ "caption": caption, **({"highlight": highlight} if highlight is not None else {}), "photos": [(io.BytesIO(raw), "../../escape.jpg") for raw in (images or [photo()])], }) def test_authentication_and_request_limits(service): app, client, data = service assert upload(client, headers={}).status_code == 401 assert upload(client, headers={"Authorization": "Bearer wrong"}).status_code == 401 assert client.get("/api/photos/" + "a" * 64).status_code == 401 assert client.get("/healthz").status_code == 200 assert upload(client, caption="a" * 4001).status_code == 400 assert client.post("/api/photos", headers=AUTH).status_code == 400 assert upload(client, [photo()] * 21).status_code == 400 app.config["MAX_CONTENT_LENGTH"] = 100 assert upload(client).status_code == 413 assert not list((data / "submissions").iterdir()) def test_validation_is_all_or_nothing(service): _, client, data = service assert upload(client, [photo(), b"not an image"]).status_code == 400 assert upload(client, [photo(format="GIF")]).status_code == 400 assert not list((data / "submissions").iterdir()) assert not list((data / "temporary").iterdir()) def test_multi_megabyte_multipart_file(service): _, client, _ = service output = io.BytesIO() Image.frombytes("RGB", (1800, 1800), random.Random(2).randbytes(1800 * 1800 * 3)).save( output, "JPEG", quality=95) assert len(output.getvalue()) > 3 * 1024 * 1024 response = upload(client, [output.getvalue()]) assert response.status_code == 202, response.json def test_zip_batch_preserves_all_images_order_and_deduplicates(service): _, client, data = service images = [photo("red"), photo("blue"), photo("green")] archive = io.BytesIO() with zipfile.ZipFile(archive, "w") as output: for i, raw in enumerate(images): output.writestr(f"selection/{i}.jpg", raw) output.writestr("__MACOSX/._0.jpg", b"metadata") response = client.post("/api/photos", headers=AUTH, data={ "archive": (io.BytesIO(archive.getvalue()), "photos.zip"), "caption": "Batch", "highlight": "Trip", }) assert response.status_code == 202, response.json assert response.json["count"] == 3 folder = data / "submissions" / response.json["id"] for i, raw in enumerate(images): assert (folder / "originals" / str(i)).read_bytes() == raw assert upload(client, images, "Batch", highlight="Trip").json["id"] == response.json["id"] def test_zip_invalid_or_oversized_batches_are_atomic(service): _, client, data = service for contents in ([b"bad"], [photo()] * 21): archive = io.BytesIO() with zipfile.ZipFile(archive, "w") as output: for i, raw in enumerate(contents): output.writestr(f"{i}.jpg", raw) response = client.post("/api/photos", headers=AUTH, data={"archive": (io.BytesIO(archive.getvalue()), "photos.zip")}) assert response.status_code == 400 assert not list((data / "submissions").iterdir()) def test_album_caption_order_and_retry_deduplication(service): _, client, data = service caption = ' {% include secret %} {{ site.email }}\nsecond line' images = [photo("red"), photo("blue")] response = upload(client, images, caption) assert response.status_code == 202 job = response.json["id"] folder = data / "submissions" / job entry = (folder / "entry.md").read_text() assert " {% include secret %}\nhttps://example.com/?a=1&b=2' response = client.post("/api/snippets", headers=AUTH, data={"text": text}) assert response.status_code == 202 job = response.json["id"] assert response.json["url"] == f"/snippets/#snippet-{job}" assert client.get(f"/api/photos/{job}", headers=AUTH).status_code == 404 entry = (data / "submissions" / job / "entry.md").read_text() assert '