unsloth/studio/backend/tests/test_docs_ui_assets.py
Daniel Han 0bedd17d9e
Studio: serve Swagger UI and ReDoc from this origin, not a CDN (#8425)
* Studio: serve Swagger UI and ReDoc from this origin, not a CDN

FastAPI's built-in /docs and /redoc load ~2.3 MB of JavaScript from cdn.jsdelivr.net and
start Swagger with an inline script, so #8042 had to add 'unsafe-inline' and that CDN to
script-src for those paths. Those pages share an origin with the frontend, and localStorage
is origin-scoped rather than path-scoped, so anything executing there can read the access
and refresh tokens session.ts stores and call the API as that user.

Vendor the pinned bundles under backend/assets/docs_ui and re-register both pages on
FastAPI's own paths against them. Swagger's inline init runs off the per-response nonce
main.py already plumbs for the bootstrap script, so script-src stays 'self'. The docs CSP
branch now only relaxes style-src, font-src and worker-src, none of which execute script.

The pages look and behave exactly as before, and now work with no network.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Match the Swagger init tag by what follows it, not by surrounding whitespace

fastapi is unpinned, so a release that reflows the docs template or drops the comment
above the init script would miss an exact-string marker and 500 the page. Anchor on the
SwaggerUIBundle call instead.

* Docs pages: honor root_path, and ship Swagger UI's NOTICE

FastAPI's built-in docs routes prefix every URL they emit with the ASGI root_path;
the replacements did not, so behind a path-stripping proxy the schema, bundles and
OAuth redirect escaped the mapping. Apache-2.0 section 4(d) also requires the NOTICE
file to travel with a redistributed Swagger UI, along with the extracted third-party
banners its bundle names.

* Keep the vendored docs bundles out of git's CRLF conversion

The Windows cross-platform run failed the digest check on every text file in
assets/docs_ui (the PNG was fine), which is core.autocrlf=true rewriting them on
checkout. That both breaks the manifest and means a Windows install serves bytes
that are not the reviewed release.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-11 06:17:09 -07:00

77 lines
3.2 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""The vendored Swagger UI and ReDoc bundles stay byte-identical to the releases they came from.
These files execute on the Studio origin, which is where session.ts keeps the access and
refresh tokens, so the point of shipping them rather than loading them from a CDN is that
their bytes are fixed at review time. A silent edit here is a script change nobody read.
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
_BACKEND = Path(__file__).resolve().parent.parent
_DOCS_UI = _BACKEND / "assets" / "docs_ui"
_MANIFEST = json.loads((_DOCS_UI / "docs_ui_manifest.json").read_text(encoding = "utf-8"))
# Ours, not upstream's: prose we may reword, and the manifest cannot hash itself.
_UNPINNED = {"README.md", "docs_ui_manifest.json"}
def _tracked_files() -> dict[str, Path]:
"""Enumerate the real tree, so an *added* file is caught and not just an edit."""
return {
path.relative_to(_DOCS_UI).as_posix(): path
for path in sorted(_DOCS_UI.rglob("*"))
if path.is_file() and path.relative_to(_DOCS_UI).as_posix() not in _UNPINNED
}
def test_tree_matches_the_manifest():
found = _tracked_files()
recorded = _MANIFEST["files"]
assert set(found) == set(recorded), (
"assets/docs_ui gained or lost a file; it is a static copy of the pinned releases, "
"so update docs_ui_manifest.json in the same commit"
)
drifted = [
name
for name, path in found.items()
if hashlib.sha256(path.read_bytes()).hexdigest() != recorded[name]
]
assert not drifted, (
f"vendored docs assets no longer match their pinned releases: {', '.join(drifted)}. "
"A formatter or minifier most likely rewrote them"
)
def test_no_symlinks():
"""A symlink would let the digest check pass while the served bytes differ."""
offenders = [
str(path.relative_to(_DOCS_UI)) for path in _DOCS_UI.rglob("*") if path.is_symlink()
]
assert not offenders, f"assets/docs_ui must be plain files: {offenders}"
def test_every_package_ships_its_licence():
names = {entry["package"] for entry in _MANIFEST["packages"]}
assert names == {"swagger-ui-dist", "redoc"}
assert (_DOCS_UI / "LICENSE.swagger-ui").exists()
assert (_DOCS_UI / "LICENSE.redoc").exists()
# Apache-2.0 section 4(d): redistributing a work that carries a NOTICE means shipping it.
# The bundle also names its own extracted third-party banners; ship those with it.
assert (_DOCS_UI / "NOTICE.swagger-ui").exists()
assert (_DOCS_UI / "swagger-ui-bundle.js.LICENSE.txt").exists()
for entry in _MANIFEST["packages"]:
assert entry["version"] and entry["license"] and entry["source"]
def test_bundles_reference_no_remote_script_host():
"""The whole point is that nothing on the docs pages phones out for code."""
for name in ("swagger-ui-bundle.js", "redoc.standalone.js"):
text = (_DOCS_UI / name).read_text(encoding = "utf-8", errors = "ignore")
assert "cdn.jsdelivr.net" not in text, f"{name} pulls from jsDelivr at runtime"