Files
app/backend/tests/test_spa.py
T
stroblme dea25ec08d One image for the whole app: engine, API and dashboard on one port
The backend image now builds the SPA and serves it on the API's own origin
(fluksio/spa.py), so an instance is a single container with no proxy, DNS,
port 80 or .env: `docker compose up -d` with the new compose.yml runs it on
localhost:8080. It starts on fluksio serve (persisted secret key, first
admin printed once) and brings its own Redis when REDIS_HOST is unset.

Slimmer: python:3.13-slim, no dev group, uv once, fastapi without the cloud
CLI. Optional features are per-feature extras (mqtt, influx, email, sentry,
ml, server for all) picked with the EXTRAS build argument; numpy moved to
`ml`. ffmpeg left the image for imageio-ffmpeg via the Modules screen.

The operator stack runs the same image with app. and api. routed to it; the
nginx frontend service, the LAN nginx proxy and DOMAIN in the backend are
gone, and the MCP issuer is FRONTEND_HOST.
2026-10-16 11:40:02 +02:00

40 lines
1.4 KiB
Python

import base64
import hashlib
from pathlib import Path
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"<html><script>{THEME}</script></html>")
(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