secubox-deb/packages/secubox-billets/tests/test_snapshot.py
CyberMind-FR a95f157eb1 feat(billets): communiqué style + embed snapshot vignette + portable HTML archive (ref #851)
- Per-billet 'style' (default|communique). Communiqué permalink = the poster
  mockup (Space Grotesk/JetBrains Mono, ROUTE side-band, TRANSMISSION frame,
  slogans/footer); funky feed unchanged.
- Embed snapshot vignette: api/services/snapshot.py tries headless Chromium
  (guarded/optional), falls back to SSRF-guarded og:image, re-encoded via the
  media service. Captured off-loop on save (communiqué billets, cached per
  embed_url). Public render shows the vignette; click-to-load the live iframe.
- Portable single-HTML archive: GET /admin/billets/{id}/archive.html — self-
  contained (inlined CSS, system fonts, media+snapshot as base64 data-URIs,
  embed = snapshot linking to original, no iframe). Off-loop render.
- migration 0003 (style + embed_snapshot); models/repo/admin/main wired.
- CSP: style-src adds 'unsafe-inline' so the WAF-injected health-banner's
  dynamic <style> renders (was blocked → banner fell unstyled to page bottom);
  script-src stays 'self'. Google-Fonts relaxation page-scoped to communiqué.
- 118 tests (11 new: snapshot fallback/SSRF, communiqué render, archive self-containment).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 07:57:54 +02:00

94 lines
3.5 KiB
Python

# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""Embed snapshot capture: headless-Chromium path is OPTIONAL, so with the
browser simulated absent it must fall back to the SSRF-guarded og:image, and
must never fetch a private/non-public og:image."""
import io
import httpx
from PIL import Image
from api.services import snapshot
def _png_bytes(size=(320, 240)) -> bytes:
buf = io.BytesIO()
Image.new("RGB", size, (12, 74, 136)).save(buf, format="PNG")
return buf.getvalue()
def _resolver(mapping=None):
mapping = mapping or {}
def r(host, port):
return [(0, 0, 0, "", (mapping.get(host, "93.184.216.34"), port))]
return r
def test_capture_falls_back_to_og_image(tmp_path, monkeypatch):
# Simulate playwright absent / no browser binary → screenshot returns None.
monkeypatch.setattr(snapshot, "_screenshot_via_browser", lambda url: None)
png = _png_bytes()
def handler(request):
assert request.url.host == "cdn.example"
return httpx.Response(200, content=png, headers={"content-type": "image/png"})
client = httpx.Client(transport=httpx.MockTransport(handler))
res = snapshot.capture(
"https://site.example/post", "https://cdn.example/og.png",
"01SNAP0000000000000000000A", client=client, resolver=_resolver(),
directory=tmp_path, enable_browser=True)
client.close()
assert res is not None
filename, thumb = res
assert (tmp_path / filename).exists() and (tmp_path / thumb).exists()
# stored file is a valid, re-encoded image (EXIF-stripped by media.process)
Image.open(tmp_path / filename).verify()
def test_capture_returns_none_when_both_fail(tmp_path, monkeypatch):
monkeypatch.setattr(snapshot, "_screenshot_via_browser", lambda url: None)
def handler(request):
return httpx.Response(200, content=b"not an image at all",
headers={"content-type": "text/plain"})
client = httpx.Client(transport=httpx.MockTransport(handler))
res = snapshot.capture(
"https://site.example/post", "https://cdn.example/og.png",
"01SNAP0000000000000000000B", client=client, resolver=_resolver(),
directory=tmp_path, enable_browser=True)
client.close()
assert res is None
assert list(tmp_path.iterdir()) == []
def test_capture_returns_none_without_og_and_no_browser(tmp_path, monkeypatch):
monkeypatch.setattr(snapshot, "_screenshot_via_browser", lambda url: None)
res = snapshot.capture(
"https://site.example/post", None, "01SNAP0000000000000000000D",
resolver=_resolver(), directory=tmp_path, enable_browser=True)
assert res is None
def test_capture_ssrf_rejects_private_og(tmp_path, monkeypatch):
monkeypatch.setattr(snapshot, "_screenshot_via_browser", lambda url: None)
calls = {"n": 0}
def handler(request):
calls["n"] += 1
return httpx.Response(200, content=_png_bytes(),
headers={"content-type": "image/png"})
client = httpx.Client(transport=httpx.MockTransport(handler))
res = snapshot.capture(
"https://site.example/post", "https://intra.example/og.png",
"01SNAP0000000000000000000C", client=client,
resolver=_resolver({"intra.example": "10.0.0.5"}),
directory=tmp_path, enable_browser=True)
client.close()
assert res is None
assert calls["n"] == 0 # SSRF blocked BEFORE any network fetch
assert list(tmp_path.iterdir()) == []