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 '