unsloth/studio/backend/tests/test_html_preview.py
Daniel Han 169e5b3e96 Studio: trim PR 5717 in-code comments to one short sentence each
Drops 234 lines of inline commentary across the renderer, sanitizer,
HTML preview route, supply-chain gate, and their tests. Same intent,
shorter form. No code behaviour changes; vitest 24/24, pytest 24/24,
tsc / vite build clean.
2026-05-27 07:42:00 +00:00

191 lines
6.8 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
"""Tests for the /api/preview/html route (interactive HTML preview)."""
import sys
import time
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
_BACKEND_ROOT = Path(__file__).resolve().parents[1]
if str(_BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(_BACKEND_ROOT))
@pytest.fixture
def preview_app(tmp_path, monkeypatch):
"""Standalone app with only the html-preview router and a clean store."""
from auth import storage
from auth.authentication import create_access_token
monkeypatch.setattr(storage, "DB_PATH", tmp_path / "auth.db")
monkeypatch.setattr(storage, "_BOOTSTRAP_PW_PATH", tmp_path / ".bootstrap_password")
monkeypatch.setattr(storage, "_bootstrap_password", None)
import secrets as _secrets
storage.create_initial_user(
username = storage.DEFAULT_ADMIN_USERNAME,
password = "human-password-123",
jwt_secret = _secrets.token_urlsafe(64),
must_change_password = False,
)
from routes.html_preview import router as html_preview_router
from routes import html_preview as html_preview_module
html_preview_module._PREVIEWS.clear() # fresh store per test
app = FastAPI()
app.include_router(
html_preview_router, prefix = "/api/preview/html", tags = ["html-preview"]
)
token = create_access_token(storage.DEFAULT_ADMIN_USERNAME)
return app, token, html_preview_module
class TestPostHtmlPreview:
def test_post_requires_auth(self, preview_app):
app, _token, _mod = preview_app
c = TestClient(app)
r = c.post("/api/preview/html", json = {"source": "<h1>hi</h1>"})
assert r.status_code in (401, 403)
def test_post_returns_same_origin_url_and_ttl(self, preview_app):
app, token, _mod = preview_app
c = TestClient(app)
r = c.post(
"/api/preview/html",
json = {"source": "<h1>hello</h1>"},
headers = {"Authorization": f"Bearer {token}"},
)
assert r.status_code == 200
body = r.json()
assert body["url"].startswith("/api/preview/html/")
assert isinstance(body["expires_in_seconds"], int)
assert body["expires_in_seconds"] > 0
def test_post_rejects_oversize_body(self, preview_app):
app, token, mod = preview_app
c = TestClient(app)
too_big = "x" * (mod.MAX_HTML_PREVIEW_BYTES + 1)
r = c.post(
"/api/preview/html",
json = {"source": too_big},
headers = {"Authorization": f"Bearer {token}"},
)
# Pydantic's max_length surfaces as 422; we accept any non-2xx
# as long as nothing is stored.
assert r.status_code >= 400
assert mod._PREVIEWS == {}
def test_post_returns_unguessable_tokens(self, preview_app):
# Identical source -> distinct tokens.
app, token, _mod = preview_app
c = TestClient(app)
urls = set()
for _ in range(5):
r = c.post(
"/api/preview/html",
json = {"source": "<p>same</p>"},
headers = {"Authorization": f"Bearer {token}"},
)
assert r.status_code == 200
urls.add(r.json()["url"])
assert len(urls) == 5
class TestGetHtmlPreview:
def _create(self, app, token, source):
c = TestClient(app)
r = c.post(
"/api/preview/html",
json = {"source": source},
headers = {"Authorization": f"Bearer {token}"},
)
return r.json()["url"]
def test_get_serves_stored_html_with_overriding_csp(self, preview_app):
app, token, _mod = preview_app
url = self._create(app, token, "<button onclick=\"alert('x')\">go</button>")
c = TestClient(app)
r = c.get(url)
assert r.status_code == 200
body = r.text
assert "<!doctype html>" in body.lower()
assert '<base target="_blank">' in body
assert "<button onclick=\"alert('x')\">go</button>" in body
csp = r.headers["content-security-policy"]
directives = {
chunk.strip().split(" ", 1)[0]: chunk.strip()
for chunk in csp.split(";")
if chunk.strip()
}
# Inline scripts allowed, network egress closed.
assert "'none'" in directives["default-src"]
assert "'unsafe-inline'" in directives["script-src"]
assert "'none'" in directives["connect-src"]
assert "'none'" in directives["frame-src"]
# Same-origin embedders only.
assert "'self'" in directives["frame-ancestors"]
# Override the global DENY so host chat can iframe us.
assert r.headers["x-frame-options"].upper() == "SAMEORIGIN"
assert "no-store" in r.headers["cache-control"]
def test_get_is_not_auth_gated(self, preview_app):
# Iframe subresource loads do not carry Authorization; the URL
# token is the authorisation.
app, token, _mod = preview_app
url = self._create(app, token, "<p>nope</p>")
c = TestClient(app)
r = c.get(url) # no Authorization header
assert r.status_code == 200
def test_get_unknown_token_is_404(self, preview_app):
app, _token, _mod = preview_app
c = TestClient(app)
r = c.get("/api/preview/html/totally-not-a-real-token")
assert r.status_code == 404
def test_get_expired_token_is_404(self, preview_app):
app, token, mod = preview_app
url = self._create(app, token, "<p>aging</p>")
# Rewind monotonic past the TTL.
token_id = url.rsplit("/", 1)[-1]
created, src = mod._PREVIEWS[token_id]
mod._PREVIEWS[token_id] = (created - (mod.PREVIEW_TTL_SECONDS + 5), src)
c = TestClient(app)
r = c.get(url)
assert r.status_code == 404
# Swept on access.
assert token_id not in mod._PREVIEWS
class TestEviction:
def test_overflow_evicts_oldest_entries(self, preview_app):
app, token, mod = preview_app
c = TestClient(app)
mod.MAX_LIVE_PREVIEWS = 4 # cheap test
urls = []
for i in range(6):
r = c.post(
"/api/preview/html",
json = {"source": f"<p>{i}</p>"},
headers = {"Authorization": f"Bearer {token}"},
)
urls.append(r.json()["url"])
time.sleep(0.001) # deterministic monotonic order
assert len(mod._PREVIEWS) == mod.MAX_LIVE_PREVIEWS
# Oldest two evicted.
for old in urls[:2]:
token_id = old.rsplit("/", 1)[-1]
assert token_id not in mod._PREVIEWS
# Newest survive.
for fresh in urls[-mod.MAX_LIVE_PREVIEWS :]:
token_id = fresh.rsplit("/", 1)[-1]
assert token_id in mod._PREVIEWS