placeholder-api/tests/test_api.py

107 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import struct
from xml.etree import ElementTree
import pytest
from app import config
@pytest.fixture(params=["svg", "png"])
def image_url(request):
return f"/placeholder.{request.param}"
def test_health(client):
response = client.get("/healthz")
assert response.status_code == 200
assert response.json == {"status": "ok"}
assert "Cache-Control" not in response.headers
@pytest.mark.parametrize("custom_dimensions", [False, True])
def test_image_output(client, image_url, custom_dimensions):
parameters = {"width": 321, "height": 123, "font_size": 17} if custom_dimensions else {}
width = parameters.get("width", config.DEFAULT_IMAGE_WIDTH)
height = parameters.get("height", config.DEFAULT_IMAGE_HEIGHT)
font_size = parameters.get("font_size", config.DEFAULT_FONT_SIZE)
response = client.get(image_url, query_string=parameters)
assert response.status_code == 200
if image_url.endswith(".svg"):
assert response.mimetype == "image/svg+xml"
root = ElementTree.fromstring(response.data) # noqa: S314 - trusted SVG generated by the app
assert root.tag == "{http://www.w3.org/2000/svg}svg"
assert root.attrib["width"] == str(width)
assert root.attrib["height"] == str(height)
text = root.find("{http://www.w3.org/2000/svg}text")
assert text is not None
assert text.attrib["font-size"] == str(font_size)
assert text.text == f"{width} × {height}"
else:
assert response.mimetype == "image/png"
assert response.data[:8] == b"\x89PNG\r\n\x1a\n"
assert response.data[12:16] == b"IHDR"
# IHDR starts with width and height as big-endian unsigned 32-bit integers.
assert struct.unpack(">II", response.data[16:24]) == (width, height)
@pytest.mark.parametrize(
("parameter", "maximum"),
[
("width", config.MAX_IMAGE_WIDTH),
("height", config.MAX_IMAGE_HEIGHT),
("font_size", config.MAX_FONT_SIZE),
],
)
@pytest.mark.parametrize("boundary", ["below", "minimum", "maximum", "above"])
def test_parameter_bounds(client, image_url, parameter, maximum, boundary):
value = {"below": 0, "minimum": 1, "maximum": maximum, "above": maximum + 1}[boundary]
# Vary one bound at a time, keeping the other dimensions small for cheap renders.
parameters = {"width": 10, "height": 10, "font_size": 1, parameter: value}
response = client.get(image_url, query_string=parameters)
if boundary in {"minimum", "maximum"}:
assert response.status_code == 200
else:
assert response.status_code == 400
assert response.json == {
"status": "error",
"message": f"{parameter} outside of allowable range",
}
assert "Cache-Control" not in response.headers
@pytest.mark.parametrize("timeout", [config.CACHE_DEFAULT_TIMEOUT, 120])
def test_image_cache_control(app, client, image_url, timeout):
# An override catches headers that hard-code the default timeout.
app.config["CACHE_DEFAULT_TIMEOUT"] = timeout
response = client.get(image_url)
assert response.status_code == 200
assert response.cache_control.public
assert response.cache_control.max_age == timeout
assert not response.cache_control.no_cache
assert not response.cache_control.no_store
def test_cached_image_survives_render_limit(client, image_url, monkeypatch):
limit_name = "SVG_RENDER_LIMIT" if image_url.endswith(".svg") else "PNG_RENDER_LIMIT"
# Exhaust the render quota with one request; the request quota remains available.
monkeypatch.setattr(config, limit_name, "1/minute")
first = client.get(image_url, query_string={"width": 10, "height": 10})
assert first.status_code == 200
# A new size needs a render, but the original size should still come from cache.
uncached = client.get(image_url, query_string={"width": 11, "height": 10})
assert uncached.status_code == 429
assert "Cache-Control" not in uncached.headers
cached = client.get(image_url, query_string={"width": 10, "height": 10})
assert cached.status_code == 200
assert cached.data == first.data