import io import hashlib import json from datetime import datetime, timezone 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): if exif is None: exif = Image.Exif() exif[36867] = "2025:01:02 03:04:05" exif[36881] = "+00:00" 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, data = service output = io.BytesIO() exif = Image.Exif() exif[36867] = "2025:01:02 03:04:05" Image.frombytes("RGB", (1800, 1800), random.Random(2).randbytes(1800 * 1800 * 3)).save( output, "JPEG", quality=95, exif=exif) assert len(output.getvalue()) > 3 * 1024 * 1024 response = upload(client, [output.getvalue()]) assert response.status_code == 202, response.json path = data / "submissions" / response.json["id"] / "images/00.jpg" assert path.stat().st_size <= 300 * 1024 with Image.open(path) as image: assert max(image.size) <= 1600 assert image.info.get("progressive") 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 entries = sorted((folder / "entries").glob("*.md")) assert len(entries) == 2 rendered = "\n".join(entry.read_text() for entry in entries) 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 '