import base64 import gzip import hashlib from pathlib import Path import pytest from starlette.applications import Starlette from starlette.responses import PlainTextResponse from starlette.routing import Route from starlette.testclient import TestClient from fluksio.spa import Dashboard THEME = "document.documentElement.classList.add('dark')" def _client(dist: Path) -> TestClient: (dist / "assets").mkdir(parents=True) (dist / "index.html").write_text(f"") (dist / "assets" / "app-1a2b.js").write_text("console.log(1)") api = Starlette(routes=[Route("/api/v1/ping", lambda _: PlainTextResponse("api"))]) return TestClient(Dashboard(api, dist)) def test_the_dashboard_and_the_api_share_one_origin(tmp_path: Path) -> None: client = _client(tmp_path) # A client-side route survives a reload, with the inline script admitted. page = client.get("/flows/house") digest = base64.b64encode(hashlib.sha256(THEME.encode()).digest()).decode() assert THEME in page.text assert page.headers["cache-control"] == "no-cache" assert f"'sha256-{digest}'" in page.headers["content-security-policy"] asset = client.get("/assets/app-1a2b.js") assert asset.text == "console.log(1)" assert "immutable" in asset.headers["cache-control"] assert client.get("/api/v1/ping").text == "api" # Nothing outside the build directory is served, only the shell. assert THEME in client.get("/assets/../../etc/passwd").text @pytest.mark.parametrize( ("accepted", "compressed"), [ ("gzip", True), ("br, gzip;q=0.8", True), ("gzip ; q=1, identity;q=0", True), ("*", True), ("gzip;q=0, *;q=1", False), ("gzip;q=0.5, identity;q=1", False), ("br", False), ("identity", False), ], ) def test_precompressed_assets_keep_their_type_and_negotiate_encoding( tmp_path: Path, accepted: str, compressed: bool ) -> None: client = _client(tmp_path) content = b"console.log('built once');\n" * 200 asset = tmp_path / "assets" / "app-1a2b.js" asset.write_bytes(content) packed = gzip.compress(content) asset.with_name(asset.name + ".gz").write_bytes(packed) response = client.get("/assets/app-1a2b.js", headers={"Accept-Encoding": accepted}) assert response.content == content assert (response.headers.get("content-encoding") == "gzip") is compressed assert "javascript" in response.headers["content-type"] assert response.headers["vary"] == "Accept-Encoding" assert "immutable" in response.headers["cache-control"] expected = len(packed) if compressed else len(content) assert int(response.headers["content-length"]) == expected head = client.head("/assets/app-1a2b.js", headers={"Accept-Encoding": accepted}) assert head.content == b"" assert head.headers["content-length"] == str(expected) def test_a_build_without_compressed_assets_still_serves_plain_files( tmp_path: Path, ) -> None: client = _client(tmp_path) content = b"console.log('plain build');\n" * 200 (tmp_path / "assets" / "app-1a2b.js").write_bytes(content) response = client.get("/assets/app-1a2b.js", headers={"Accept-Encoding": "gzip"}) assert response.content == content assert "content-encoding" not in response.headers