feat(release): publish minimal self-contained Magent source

This commit is contained in:
Magent release tooling
2026-09-19 16:58:12 +12:00
commit 5fa5d45535
272 changed files with 79305 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
"""Check the environment reference against source without importing application settings.
Only tracked-source locations are inspected. Deployment .env files, process
environment values and runtime data are never opened or evaluated.
"""
import ast
from dataclasses import dataclass
import json
from pathlib import Path
import re
import sys
ROOT = Path(__file__).resolve().parents[1]
ENV_NAME = re.compile(r"[A-Z][A-Z0-9_]*\Z")
@dataclass(frozen=True)
class Setting:
names: tuple[str, ...]
default: str
def settings_inventory(source: str) -> list[Setting]:
tree = ast.parse(source.lstrip("\ufeff"))
settings = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "Settings")
result = []
for node in settings.body:
if not isinstance(node, ast.AnnAssign) or not isinstance(node.target, ast.Name):
continue
name = node.target.id
names = (name.upper(),)
default = node.value
if isinstance(default, ast.Call):
arguments = {keyword.arg: keyword.value for keyword in default.keywords}
alias = arguments.get("validation_alias")
if isinstance(alias, ast.Constant):
names = (alias.value,)
elif isinstance(alias, ast.Call):
names = tuple(ast.literal_eval(argument) for argument in alias.args)
default = arguments.get("default")
if isinstance(default, ast.Name):
value = "@" + default.id
else:
value = json.dumps(ast.literal_eval(default), ensure_ascii=True)
result.append(Setting(names, value))
return result
def python_environment_names(source: str) -> set[str]:
names = set()
for node in ast.walk(ast.parse(source.lstrip("\ufeff"))):
argument = None
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.args:
receiver = ast.unparse(node.func.value)
if (node.func.attr == "getenv" and receiver == "os") or (
node.func.attr == "get" and receiver in {"os.environ", "environ", "environment", "prepared"}
):
argument = node.args[0]
elif isinstance(node, ast.Subscript) and ast.unparse(node.value) in {
"os.environ", "environ", "environment", "prepared"
}:
argument = node.slice
if isinstance(argument, ast.Constant) and isinstance(argument.value, str) and ENV_NAME.fullmatch(argument.value):
names.add(argument.value)
return names
def runtime_environment_names(root: Path) -> set[str]:
names = set()
sources = [*root.glob("backend/app/**/*.py"), *root.glob("scripts/*.py")]
for path in sources:
names.update(python_environment_names(path.read_text(encoding="utf-8")))
javascript = [*root.glob("frontend/app/**/*.ts"), *root.glob("frontend/app/**/*.tsx"),
*root.glob("scripts/*.cjs"), root / "frontend/proxy.ts", root / "frontend/next.config.js"]
for path in javascript:
if ".test." not in path.name:
names.update(re.findall(r"process\.env\.([A-Z][A-Z0-9_]*)", path.read_text(encoding="utf-8")))
deployment = [*root.glob("*compose*.yml"), *root.glob("scripts/*.sh"), *root.glob("scripts/*.ps1"),
*root.glob(".gitea/workflows/*.yml")]
for path in deployment:
source = path.read_text(encoding="utf-8")
names.update(re.findall(r"\$\{([A-Z][A-Z0-9_]*)", source))
names.update(re.findall(r"\$env:([A-Z][A-Z0-9_]*)", source))
names.update(re.findall(r"secrets\.([A-Z][A-Z0-9_]*)", source))
# These are shell syntax/builtins, not Magent configuration options.
names.difference_update({"BASH_SOURCE", "HOME", "RANDOM"})
dockerfile = (root / "Dockerfile").read_text(encoding="utf-8").replace("\\\n", " ")
for line in dockerfile.splitlines():
if line.startswith(("ENV ", "ARG ")):
names.update(re.findall(r"\b([A-Z][A-Z0-9_]*)=", line))
supervisor = (root / "docker/supervisord.conf").read_text(encoding="utf-8")
for line in supervisor.splitlines():
if line.startswith("environment="):
names.update(re.findall(r"\b([A-Z][A-Z0-9_]*)=", line))
return names
def check_documentation(root: Path = ROOT) -> tuple[list[str], int]:
document = (root / "docs/ENVIRONMENT.md").read_text(encoding="utf-8")
documented = set(re.findall(r"`([A-Z][A-Z0-9_]*)`", document))
settings = settings_inventory((root / "backend/app/config.py").read_text(encoding="utf-8"))
required = runtime_environment_names(root) | {name for setting in settings for name in setting.names}
errors = [f"Undocumented environment variable: {name}" for name in sorted(required - documented)]
defaults = {}
for line in document.splitlines():
cells = line.split("|")
if len(cells) >= 4 and cells[1].strip().startswith("`"):
for name in re.findall(r"`([A-Z][A-Z0-9_]*)`", cells[1]):
defaults[name] = cells[2].strip().strip("`")
for setting in settings:
for name in setting.names:
if name in documented and defaults.get(name) != setting.default:
errors.append(f"Stale source default for {name}: expected {setting.default!r}, documented {defaults.get(name)!r}")
return errors, len(required)
def main() -> int:
errors, count = check_documentation()
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
print(f"Environment documentation covers {count} source-declared variables; Settings defaults match.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
python_bin="${PYTHON_BIN:-python3}"
echo "Installing backend Python requirements and quality tools"
"$python_bin" -m pip install -r backend/requirements-dev.txt
echo "Running Python dependency integrity check"
"$python_bin" -m pip check
echo "Auditing Python production dependencies"
"$python_bin" -m pip_audit -r backend/requirements.txt --progress-spinner off
"$python_bin" -m pip_audit -r docker/requirements-runtime.txt --progress-spinner off
echo "Linting backend application code"
"$python_bin" -m ruff check backend/app scripts/container_smoke.py scripts/check_environment_docs.py backend/tests/test_container_packaging.py backend/tests/test_container_bootstrap.py backend/tests/test_managed_setup_origin.py backend/tests/test_environment_docs.py
echo "Running backend unit tests with coverage"
"$python_bin" -m coverage erase
"$python_bin" -m coverage run -m unittest discover -s backend/tests -p "test_*.py" -v
"$python_bin" -m coverage report
echo "Backend quality gate passed"
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# No argument preserves the CI build-and-test entry point. Pass an image tag to
# test an already-built release without building, pulling, or publishing it.
set -euo pipefail
if [ "$#" -gt 1 ]; then
echo "Usage: $0 [existing-image]" >&2
exit 2
fi
script_directory="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repository_directory="$(cd -- "$script_directory/.." && pwd)"
image="${1:-magent:ci}"
size_limit_mb="${MAGENT_IMAGE_MAX_MB:-350}"
managed_mode="${MAGENT_SMOKE_MANAGED:-false}"
if [[ "$managed_mode" != true && "$managed_mode" != false ]]; then
echo "MAGENT_SMOKE_MANAGED must be true or false." >&2
exit 2
fi
if ! [[ "$size_limit_mb" =~ ^[1-9][0-9]*$ ]]; then
echo "MAGENT_IMAGE_MAX_MB must be a positive integer (MiB)." >&2
exit 2
fi
container_name="magent-ci-${GITHUB_RUN_ID:-local}-$$-${RANDOM}"
volume_name="${container_name}-data"
network_name="${container_name}-isolated"
container_created=false
volume_created=false
network_created=false
cleanup() {
result=$?
trap - EXIT
if [ "$result" -ne 0 ] && [ "$container_created" = true ]; then
# Only synthetic credentials/data enter this test container.
docker logs --tail 100 "$container_name" >&2 || true
fi
if [ "$container_created" = true ]; then
docker rm -f "$container_name" >/dev/null 2>&1 || true
fi
if [ "$volume_created" = true ]; then
docker volume rm "$volume_name" >/dev/null 2>&1 || true
fi
if [ "$network_created" = true ]; then
docker network rm "$network_name" >/dev/null 2>&1 || true
fi
exit "$result"
}
trap cleanup EXIT
if [ "$#" -eq 0 ]; then
docker build --tag "$image" "$repository_directory"
fi
image_size="$(docker image inspect --format '{{.Size}}' "$image")"
image_id="$(docker image inspect --format '{{.Id}}' "$image")"
if [ "$image_size" -gt "$((size_limit_mb * 1024 * 1024))" ]; then
echo "Image exceeds ${size_limit_mb} MiB unpacked budget: ${image_size} bytes" >&2
exit 1
fi
echo "Image size: ${image_size} bytes (budget ${size_limit_mb} MiB unpacked)"
# Inspect the image's original filesystem before tmpfs or volume mounts could
# hide accidentally shipped build caches or private files.
docker run --rm --pull never --network none --read-only \
--cap-drop ALL --security-opt no-new-privileges:true \
--entrypoint python -i "$image_id" - packaging < "$script_directory/container_smoke.py"
# An internal network prevents accidental external integration calls. No host
# files, existing volumes, host credentials, or host ports are used.
docker network create --internal "$network_name" >/dev/null
network_created=true
docker volume create "$volume_name" >/dev/null
volume_created=true
start_container() {
local -a secret_environment
if [ "$managed_mode" = true ]; then
# Exercise the image defaults: no keys, origin or managed-mode variables.
secret_environment=()
else
secret_environment=(
--env JWT_SECRET=ci-only-secret-with-at-least-32-characters
--env SETTINGS_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
--env SETUP_TOKEN=ci-only-setup-token-with-at-least-32-characters
--env AUTH_COOKIE_SECURE=true
--env MAGENT_APPLICATION_URL=https://magent-ci.example.test
)
fi
docker run --detach --name "$container_name" --pull never \
--network "$network_name" \
--read-only --cap-drop ALL --security-opt no-new-privileges:true \
--pids-limit 256 --memory 1g --cpus 2 \
--tmpfs /tmp:rw,noexec,nosuid,size=64m,uid=1000,gid=1000 \
--tmpfs /app/frontend/.next/cache:rw,noexec,nosuid,size=128m,uid=1000,gid=1000 \
--volume "$volume_name:/app/data" \
"${secret_environment[@]}" \
--env ADMIN_PASSWORD= \
--env AUTH_COOKIE_SAMESITE=strict \
--env BACKGROUND_TASKS_ENABLED=false \
--env MAGENT_METRICS_ENABLED=false \
"$image_id" >/dev/null
container_created=true
}
wait_for_health() {
local deadline=$((SECONDS + 150))
local status
while true; do
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$container_name")"
if [ "$status" = healthy ]; then
return
fi
if [ "$status" = missing ] || [ "$SECONDS" -ge "$deadline" ]; then
echo "Container did not become healthy within 150 seconds (status: $status)" >&2
return 1
fi
if [ "$(docker inspect --format '{{.State.Running}}' "$container_name")" != true ]; then
echo "Container exited before becoming healthy" >&2
return 1
fi
sleep 2
done
}
# Deliberately no root/chown helper: the image must initialize a fresh named
# volume with correct ownership for its normal non-root runtime user.
start_container
wait_for_health
docker exec -i "$container_name" python - fresh < "$script_directory/container_smoke.py"
docker restart --time 15 "$container_name" >/dev/null
wait_for_health
docker exec -i "$container_name" python - persisted < "$script_directory/container_smoke.py"
# Recreation proves database/configuration are in the volume, not merely in the
# container's writable layer. Both test instances use the same immutable image.
docker rm -f "$container_name" >/dev/null
container_created=false
start_container
wait_for_health
docker exec -i "$container_name" python - persisted < "$script_directory/container_smoke.py"
echo "Container smoke passed: fresh install, security headers, assets, login, backup/restore, restart, recreation."
+340
View File
@@ -0,0 +1,340 @@
"""Disposable-image checks, streamed into the container by ci_container_smoke.sh.
Uses only Python's standard library. All credentials and configuration below
are synthetic and the caller disables network egress and background workers.
This checks script/asset delivery and CSP compatibility, not browser execution.
"""
from html.parser import HTMLParser
import hashlib
from http.cookies import SimpleCookie
import json
import os
from pathlib import Path
import re
import secrets
import shutil
import sqlite3
import subprocess
import sys
from urllib import error, parse, request
ORIGIN = "https://magent-ci.example.test"
FRONTEND = "http://127.0.0.1:3000"
SETUP_TOKEN = "ci-only-setup-token-with-at-least-32-characters"
ADMIN_USERNAME = "container-smoke-admin"
ADMIN_PASSWORD = "Container-smoke-owner-password-123456789!"
INTEGRATION_SECRET = "synthetic-container-smoke-integration-key"
LOGIN_MESSAGE = "Welcome to an independent Magent installation"
BACKUP_PASSPHRASE = "Synthetic container backup passphrase only"
CACHE_FIXTURE = Path("/app/data/artwork/tmdb/w342/container-smoke.jpg")
CACHE_CONTENT = b"synthetic artwork cache fixture"
def managed_installation() -> bool:
return os.environ.get("MAGENT_MANAGED_SECRETS") in {"true", "auto"} and not os.environ.get("JWT_SECRET")
def check(condition: bool, message: str) -> None:
if not condition:
raise AssertionError(message)
def http(
path: str,
*,
expected: int = 200,
method: str = "GET",
payload: dict | None = None,
form: dict | None = None,
raw: bytes | None = None,
headers: dict | None = None,
base: str = FRONTEND,
) -> tuple[bytes, object]:
outgoing_headers = {"Origin": ORIGIN, **(headers or {})}
check(sum(value is not None for value in (payload, form, raw)) <= 1,
"HTTP body must use only one encoding")
data = raw
if payload is not None:
data = json.dumps(payload).encode()
outgoing_headers["Content-Type"] = "application/json"
elif form is not None:
data = parse.urlencode(form).encode()
outgoing_headers["Content-Type"] = "application/x-www-form-urlencoded"
probe = request.Request(base + path, data=data, headers=outgoing_headers, method=method)
try:
response = request.urlopen(probe, timeout=30)
except error.HTTPError as exc:
response = exc
with response:
check(response.status == expected, f"{method} {path}: expected {expected}, got {response.status}")
return response.read(), response.headers
def api(path: str, **kwargs) -> dict:
body, _ = http("/api" + path, **kwargs)
return json.loads(body)
class PageAssets(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.scripts: list[dict] = []
self.assets: set[str] = set()
def handle_starttag(self, tag: str, attributes: list) -> None:
values = dict(attributes)
if tag == "script":
self.scripts.append(values)
if values.get("src"):
self.assets.add(values["src"])
if tag == "link" and values.get("href", "").startswith("/_next/static/"):
self.assets.add(values["href"])
def check_page(path: str, asset_cache: set[str]) -> str:
body, headers = http(path)
check("text/html" in headers.get("Content-Type", ""), f"{path} is not HTML")
policy = headers.get("Content-Security-Policy", "")
match = re.search(r"script-src [^;]*'nonce-([^']+)'", policy)
check(match is not None, f"{path} missing script nonce policy")
nonce = match.group(1)
check("'strict-dynamic'" in policy, f"{path} lost strict-dynamic")
check("'unsafe-eval'" not in policy, f"{path} enables development eval")
check(headers.get("X-Content-Type-Options") == "nosniff", "Missing nosniff header")
check(headers.get("X-Frame-Options") == "DENY", "Missing anti-framing header")
check(headers.get("X-Powered-By") is None, "Frontend exposes its framework")
parsed = PageAssets()
parsed.feed(body.decode())
executable_scripts = [
script for script in parsed.scripts
if script.get("type", "").lower() in ("", "module", "text/javascript", "application/javascript")
]
check(bool(executable_scripts), f"{path} contains no frontend bootstrap scripts")
for script in executable_scripts:
check(script.get("nonce") == nonce, f"{path} contains a script blocked by its CSP nonce")
check(any(asset.startswith("/_next/static/") and ".js" in asset for asset in parsed.assets),
f"{path} contains no static JavaScript assets")
for asset in sorted(parsed.assets - asset_cache):
check(asset.startswith("/_next/static/"), f"Unexpected external executable asset on {path}")
content, asset_headers = http(asset)
check(bool(content), f"Empty static asset: {asset}")
check("text/html" not in asset_headers.get("Content-Type", ""), f"Asset returned HTML: {asset}")
asset_cache.add(asset)
return nonce
def check_packaging() -> None:
check(os.getuid() == 1000 and os.getgid() == 1000, "Runtime is not the default non-root UID/GID 1000")
check(Path("/app/frontend/server.js").is_file(), "Missing standalone frontend server")
check(shutil.which("node") == "/usr/local/bin/node", "Node is not the standalone runtime binary")
check(shutil.which("supervisord") == "/usr/local/bin/supervisord", "Missing Python supervisor")
check(shutil.which("curl") is not None, "curl compatibility for existing healthchecks was removed")
for executable in ("npm", "npx", "yarn", "pnpm", "pip", "pip3", "gcc", "g++", "make", "git", "gpg"):
check(shutil.which(executable) is None, f"Unnecessary runtime development tool: {executable}")
for forbidden in (
"/app/.git", "/app/tests", "/app/app/tests", "/app/backend/tests",
"/app/frontend/app", "/app/frontend/tsconfig.json", "/app/frontend/proxy.ts",
"/app/frontend/node_modules/typescript", "/app/frontend/node_modules/eslint",
"/app/frontend/node_modules/vitest", "/app/frontend/node_modules/@playwright",
"/app/frontend/node_modules/@biomejs",
"/app/frontend/node_modules/@next/swc-linux-x64-gnu",
"/app/frontend/node_modules/@next/swc-linux-arm64-gnu",
"/app/frontend/node_modules/@next/swc-linux-x64-musl",
"/app/frontend/node_modules/@next/swc-linux-arm64-musl",
"/root/.npm", "/root/.cache/pip", "/usr/local/lib/node_modules/npm",
):
check(not Path(forbidden).exists(), f"Unnecessary build/private artifact: {forbidden}")
for directory in (Path("/app"), Path("/app/frontend")):
check(not any(directory.glob(".env*")), f"Private environment file in {directory}")
check(not Path("/app/data/bootstrap-secrets.json").exists(), "Managed secrets baked into image")
check(not any(Path("/app/frontend/.next/cache").iterdir()), "Frontend build cache shipped in runtime")
check(Path("/usr/share/licenses/magent/LICENSE").is_file(), "Magent license is missing")
check(Path("/usr/local/share/doc/nodejs/LICENSE").is_file(), "Node distribution license is missing")
check(Path("/usr/share/licenses/magent/frontend/dependencies.json").is_file(),
"Frontend dependency inventory is missing")
print("Standalone packaging, non-root runtime and absent development tools/private files: PASS")
def check_runtime() -> None:
check(os.getuid() == 1000 and os.getgid() == 1000, "Runtime is not the default non-root UID/GID 1000")
check(Path("/app/data").stat().st_uid == os.getuid(), "Fresh data volume is not owned by runtime user")
check(os.access("/app/data", os.W_OK), "Data volume is not writable")
for path in ("/api/health", "/api/setup/status"):
http(path)
http("/health", base="http://127.0.0.1:8000")
asset_cache: set[str] = set()
first_nonce = check_page("/login", asset_cache)
second_nonce = check_page("/login", asset_cache)
check(first_nonce != second_nonce, "CSP nonce is reused between requests")
check_page("/setup", asset_cache)
# Check the retained curl command because some deployed stacks override the
# image HEALTHCHECK with this exact runtime dependency.
subprocess.run(["curl", "--fail", "--silent", "--show-error", FRONTEND + "/api/health"],
check=True, stdout=subprocess.DEVNULL)
print(f"Runtime, API rewrite, CSP nonce consistency and {len(asset_cache)} static assets: PASS")
def check_origin_guards() -> None:
# MAGENT_APPLICATION_URL must permit the public origin even while CORS uses
# its localhost default. Test both direct backend and Next's API rewrite.
for base, prefix in ((FRONTEND, "/api"), ("http://127.0.0.1:8000", "")):
for endpoint in ("/auth/login", "/auth/jellyfin/login"):
for origin, expected in ((ORIGIN, 422), ("https://untrusted.example.test", 403)):
http(prefix + endpoint, base=base, method="POST", form={}, expected=expected,
headers={"Origin": origin})
print("Both login Origin guards, directly and via frontend: PASS")
def sign_in() -> dict:
_, headers = http("/api/auth/login", method="POST", form={
"username": ADMIN_USERNAME, "password": ADMIN_PASSWORD,
})
cookies = SimpleCookie()
for raw_cookie in headers.get_all("Set-Cookie", []):
cookies.load(raw_cookie)
check("magent_auth" in cookies, "Local login did not issue an authentication cookie")
auth_cookie = cookies["magent_auth"]
check(bool(auth_cookie["httponly"]), "Authentication cookie missing HttpOnly")
check(bool(auth_cookie["secure"]), "Authentication cookie missing Secure")
check(auth_cookie["samesite"].lower() == "strict", "Authentication cookie missing SameSite=strict")
# These requests traverse HTTP loopback behind the simulated HTTPS public
# origin. Forward only our synthetic cookie explicitly; never print tokens.
authenticated_headers = {"Cookie": "magent_auth=" + auth_cookie.value}
identity = api("/auth/me", headers=authenticated_headers)
check(identity["username"] == ADMIN_USERNAME and identity["role"] == "admin",
"Local administrator identity did not survive login")
return authenticated_headers
def check_persisted_settings(headers: dict) -> None:
values = {item["key"]: item for item in api("/admin/settings", headers=headers)["settings"]}
check(values["site_login_message"]["value"] == LOGIN_MESSAGE, "Public configuration did not persist")
check(values["jellyfin_api_key"]["value"] is None and values["jellyfin_api_key"]["isSet"],
"Integration secret is missing or exposed by settings API")
with sqlite3.connect("file:/app/data/magent.db?mode=ro", uri=True) as connection:
check(connection.execute("PRAGMA quick_check").fetchone()[0] == "ok", "SQLite integrity failure")
check(connection.execute("SELECT COUNT(*) FROM users").fetchone()[0] == 1,
"Fresh smoke instance has unexpected users")
stored = connection.execute("SELECT value FROM settings WHERE key = 'jellyfin_api_key'").fetchone()
check(stored is not None and INTEGRATION_SECRET not in str(stored[0]),
"Integration secret was stored without encryption")
def backup_restore_upload(content: bytes, passphrase: str) -> tuple[bytes, str]:
boundary = "magent-smoke-" + secrets.token_hex(24)
parts = []
for name, value in (("passphrase", passphrase), ("confirmation", "RESTORE")):
parts.append((f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n'
f'\r\n{value}\r\n').encode())
parts.extend([
(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="smoke.magent-backup"\r\n'
'Content-Type: application/octet-stream\r\n\r\n').encode(),
content,
f"\r\n--{boundary}--\r\n".encode(),
])
return b"".join(parts), f"multipart/form-data; boundary={boundary}"
def stage_backup_roundtrip(headers: dict) -> None:
# All paths and values belong to this disposable CI volume, never real data.
CACHE_FIXTURE.parent.mkdir(parents=True, exist_ok=True)
CACHE_FIXTURE.write_bytes(CACHE_CONTENT)
content, response_headers = http("/api/admin/backups/export", method="POST", headers=headers,
payload={"passphrase": BACKUP_PASSPHRASE, "include_cache": True})
check(content.startswith(b"MAGENT-BACKUP\x00\x01"), "Backup is not the encrypted portable format")
check(INTEGRATION_SECRET.encode() not in content, "Backup exposed plaintext integration credentials")
check(response_headers.get("Cache-Control") == "no-store", "Backup download is cacheable")
api("/admin/settings", method="PUT", headers=headers,
payload={"site_login_message": "Changed after backup"})
CACHE_FIXTURE.write_bytes(b"changed after backup")
body, content_type = backup_restore_upload(content, BACKUP_PASSPHRASE)
restored = api("/admin/backups/restore", method="POST", expected=202, raw=body,
headers={**headers, "Content-Type": content_type})
check(restored["restart_required"], "Restore did not require a restart")
status = api("/admin/backups", headers=headers)
check(status["pending_restore"] is not None, "Backup was not staged")
check(CACHE_FIXTURE.read_bytes() == b"changed after backup", "Restore applied before restart")
print("Encrypted config/database/cache backup and authenticated restore staging: PASS")
def fresh_install() -> None:
check(api("/setup/status") == {"setup_required": True, "needs_admin": True},
"Fresh volume did not open authorized first-install setup")
api("/setup/state", expected=401)
api("/admin/settings", expected=401)
token = SETUP_TOKEN
if managed_installation():
# docker exec does not inherit the entrypoint's generated environment:
# the console command must read persistent state, not rely on getenv.
token = subprocess.check_output(
[sys.executable, "-m", "app.container_bootstrap", "setup-token"], text=True,
).strip()
check(len(token) == 64 and token != SETUP_TOKEN, "Managed setup token was not generated")
state_file = Path("/app/data/bootstrap-secrets.json")
check(state_file.stat().st_mode & 0o777 == 0o600, "Managed secrets file is not private")
Path("/app/data/.smoke-managed-digest").write_text(hashlib.sha256(state_file.read_bytes()).hexdigest())
bootstrap = {"setup_token": token, "username": ADMIN_USERNAME, "password": ADMIN_PASSWORD,
"application_url": ORIGIN}
api("/setup/bootstrap", method="POST", payload=bootstrap, expected=403,
headers={"Origin": "https://untrusted.example.test"})
api("/setup/bootstrap", method="POST", payload={**bootstrap, "setup_token": "wrong-token"}, expected=403)
api("/setup/bootstrap", method="POST", payload=bootstrap, expected=201)
api("/setup/bootstrap", method="POST", payload=bootstrap, expected=409)
headers = sign_in()
check(api("/setup/state", headers=headers)["step"] == "apps", "Setup did not advance to apps")
updated = api("/admin/settings", method="PUT", headers=headers, payload={
"site_login_message": LOGIN_MESSAGE,
"jellyfin_api_key": INTEGRATION_SECRET,
})
check(updated["updated"] == 2, "Setup configuration was not saved")
api("/setup/state", method="PUT", payload={"step": "review"}, headers=headers)
check(api("/setup/complete", method="POST", headers=headers)["completed"], "Setup did not complete")
check(api("/setup/status") == {"setup_required": False, "needs_admin": False}, "Setup remained public")
check_persisted_settings(headers)
print("Token-authorized setup, local admin login, secure cookies and encrypted settings: PASS")
stage_backup_roundtrip(headers)
def persisted_install() -> None:
check(api("/setup/status") == {"setup_required": False, "needs_admin": False},
"Setup reopened after restart/recreation")
api("/setup/bootstrap", method="POST", payload={
"setup_token": SETUP_TOKEN, "username": "must-not-exist", "password": ADMIN_PASSWORD,
}, expected=409)
headers = sign_in()
check_persisted_settings(headers)
status = api("/admin/backups", headers=headers)
check(status["pending_restore"] is None, "Restore remained pending after restart")
check(status["last_restore"] and status["last_restore"]["status"] == "restored",
"Backup restore did not complete")
check(CACHE_FIXTURE.read_bytes() == CACHE_CONTENT, "Artwork cache was not restored")
if managed_installation():
state_file = Path("/app/data/bootstrap-secrets.json")
check(hashlib.sha256(state_file.read_bytes()).hexdigest()
== Path("/app/data/.smoke-managed-digest").read_text(), "Managed keys changed on restart/restore")
result = subprocess.run([sys.executable, "-m", "app.container_bootstrap", "setup-token"],
capture_output=True, text=True, check=False)
check(result.returncode != 0 and not result.stdout, "Setup token remains available after admin creation")
print("Generated keys persisted unchanged; initial setup token is no longer available: PASS")
print("Persistent setup state, administrator login, encrypted settings and database integrity: PASS")
print("Restored configuration, database and artwork cache: PASS")
def main() -> None:
check(len(sys.argv) == 2 and sys.argv[1] in ("packaging", "fresh", "persisted"),
"Expected packaging, fresh or persisted mode")
if sys.argv[1] == "packaging":
check_packaging()
return
check_runtime()
if sys.argv[1] == "fresh":
fresh_install()
else:
persisted_install()
check_origin_guards()
if __name__ == "__main__":
main()