feat(products): app-served image route — storefront-authorized, immutable cache (SD-0002 §6.4, INV-16)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:34:04 -07:00
parent 5d5341b29b
commit 7f51f5242f
4 changed files with 124 additions and 1 deletions
@@ -0,0 +1,78 @@
"""GET /api/products/images/{id}/{rendition} — authorize, stream, immutable cache (§6.4, INV-14/16)."""
import psycopg
from contextlib import contextmanager
from fastapi.testclient import TestClient
from app.main import create_app
from app.domains import products # noqa: F401 (ensures package import path)
from test_products_endpoints import _merchant_client # reuse the signed-in client
def _seed_image(fresh_db_url, status="fetched", with_detail_bytes=None, store=None,
email_storefront_only=True):
"""Insert a product + image for the (only) storefront; set keys; return (storefront_id, image_id, keys)."""
with psycopg.connect(fresh_db_url) as conn:
sf = conn.execute("SELECT id FROM storefront ORDER BY id LIMIT 1").fetchone()[0]
pid = conn.execute(
"INSERT INTO product (storefront_id, handle, title) VALUES (%s,'lamp','Lamp') RETURNING id",
(sf,)).fetchone()[0]
iid = conn.execute(
"INSERT INTO product_image (product_id, source_url, position, status)"
" VALUES (%s,'https://m/a.png',1,%s) RETURNING id", (pid, status)).fetchone()[0]
keys = {r: f"storefronts/{sf}/product-images/{iid}/{r}" for r in ("original", "thumb", "card", "detail")}
if status == "fetched":
conn.execute(
"UPDATE product_image SET key_original=%s, key_thumb=%s, key_card=%s, key_detail=%s WHERE id=%s",
(keys["original"], keys["thumb"], keys["card"], keys["detail"], iid))
conn.commit()
if with_detail_bytes is not None and store is not None:
store.put(keys["detail"], with_detail_bytes, "image/webp")
return sf, iid, keys
def test_serves_fetched_rendition_with_immutable_cache(fresh_db_url):
with _merchant_client(fresh_db_url) as client:
_sf, iid, _keys = _seed_image(fresh_db_url, status="fetched",
with_detail_bytes=b"WEBPDATA", store=client.app.state.objectstore)
resp = client.get(f"/api/products/images/{iid}/detail")
assert resp.status_code == 200
assert resp.headers["content-type"] == "image/webp"
assert "immutable" in resp.headers.get("cache-control", "")
assert resp.content == b"WEBPDATA"
def test_not_fetched_returns_409(fresh_db_url):
with _merchant_client(fresh_db_url) as client:
_sf, iid, _keys = _seed_image(fresh_db_url, status="pending")
resp = client.get(f"/api/products/images/{iid}/detail")
assert resp.status_code == 409
assert resp.json()["error"]["code"] == "not_fetched"
def test_other_storefronts_image_is_404(fresh_db_url):
# Sign in as merchant B first (this migrates the DB), then seed an image
# belonging to a *different* storefront A — it must be invisible to B.
with _merchant_client(fresh_db_url, email="b@example.com") as client:
with psycopg.connect(fresh_db_url) as conn:
a = conn.execute("INSERT INTO account (email) VALUES ('a@example.com') RETURNING id").fetchone()[0]
sfa = conn.execute("INSERT INTO storefront (name) VALUES ('A') RETURNING id").fetchone()[0]
conn.execute("INSERT INTO storefront_membership (account_id, storefront_id) VALUES (%s,%s)", (a, sfa))
pid = conn.execute("INSERT INTO product (storefront_id, handle, title) VALUES (%s,'lamp','Lamp') RETURNING id", (sfa,)).fetchone()[0]
iid = conn.execute("INSERT INTO product_image (product_id, source_url, position, status) VALUES (%s,'u',1,'fetched') RETURNING id", (pid,)).fetchone()[0]
conn.commit()
resp = client.get(f"/api/products/images/{iid}/detail")
assert resp.status_code == 404
def test_unauthenticated_is_401(fresh_db_url):
with TestClient(create_app(database_url=fresh_db_url)) as client:
assert client.get("/api/products/images/1/detail").status_code == 401
def test_bad_rendition_is_422(fresh_db_url):
with _merchant_client(fresh_db_url) as client:
_sf, iid, _keys = _seed_image(fresh_db_url, status="fetched",
with_detail_bytes=b"x", store=client.app.state.objectstore)
assert client.get(f"/api/products/images/{iid}/huge").status_code == 422