feat(bundled-binary): add a test

This commit is contained in:
Edouard Vanbelle
2026-08-29 02:17:02 +02:00
parent e562a3de30
commit 5c956c9bde
8 changed files with 412 additions and 29 deletions
+100
View File
@@ -176,6 +176,21 @@ jobs:
with: with:
components: clippy components: clippy
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
# `--all-features` includes `bundled-assets`, whose build.rs guard
# requires `static-dist/index.html` at compile time (rust-embed
# scans the folder). Build the SPA first so the lint pass covers
# the embed code paths without needing to enumerate features
# around it. ~90 s once, cached by npm-cache on repeats.
- uses: actions/setup-node@v4
with:
node-version: 26.3.0
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Build SPA (needed for --all-features / bundled-assets)
working-directory: frontend
run: npm ci && npm run build
- run: cargo clippy --all-targets --all-features -- -D warnings - run: cargo clippy --all-targets --all-features -- -D warnings
# Mirrors the `wasm-check` justfile recipe. The wasm crate is a # Mirrors the `wasm-check` justfile recipe. The wasm crate is a
@@ -306,6 +321,18 @@ jobs:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
# `--all-features` enables `bundled-assets`, whose build.rs guard
# requires `static-dist/index.html` at compile time. Build the SPA
# first so tests can compile the embed code paths. ~90 s, cached.
- uses: actions/setup-node@v4
with:
node-version: 26.3.0
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Build SPA (needed for --all-features / bundled-assets)
working-directory: frontend
run: npm ci && npm run build
- name: Initialize test database - name: Initialize test database
# Applies every migration + seeds the integration-test admin row. # Applies every migration + seeds the integration-test admin row.
# Same script used by `just test-integration` locally. # Same script used by `just test-integration` locally.
@@ -460,6 +487,79 @@ jobs:
path: tests/api/storage/ path: tests/api/storage/
retention-days: 7 retention-days: 7
bundled-binary-test:
# `--features bundled-assets` end-to-end integration test.
#
# Builds oxicloud with the SPA baked in via rust-embed, boots it
# against a nonexistent OXICLOUD_STATIC_PATH so the embed path is
# forced, and asserts SPA + locales + immutable-cache headers +
# CSP all serve correctly from the embedded corpus. Guards against
# three failure classes that don't surface in filesystem-served CI:
#
# 1. rust-embed configuration (glob patterns silently producing a
# 0-file embed — hit 2026-08-28).
# 2. Debug-vs-release drift (rust-embed's dynamic-read mode in
# debug builds masks embed bugs; `debug-embed` feature bakes
# files in for both profiles).
# 3. Axum `Path` extractor on fallback routes returning 500 (the
# `serve_root` handler needs `Request` extraction — hit 2026-08-28).
#
# See tests/bundled-binary/run.sh + docs/plan/bundled-binary.md § 2.
#
# Doesn't reuse the `build` job's artifact because that binary is
# compiled with `--features plugins`, not `--features bundled-assets`
# — different feature set = different target. `Swatinem/rust-cache`
# still shares dependency compilation between the two jobs.
name: Bundled-assets binary — embed + SPA-serve integration
needs: changes
if: |
github.event_name == 'pull_request' &&
(needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true')
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
# Same disk-hygiene pattern as the `build` job — cargo release
# link + full node_modules install would otherwise squeeze the
# runner disk budget under peak concurrency.
- uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: false
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4
with:
node-version: 26.3.0
cache: npm
cache-dependency-path: frontend/package-lock.json
# Build the SPA up front so the test script can run with
# `SKIP_FRONTEND_BUILD=1` — makes the test-runner log clean
# (no duplicated npm ci noise) and puts the SPA build's cost
# in its own step for CI-side timing visibility.
- name: Build SPA (Vite → static-dist/)
working-directory: frontend
run: npm ci && npm run build
- name: Run bundled-binary integration test
run: bash tests/bundled-binary/run.sh
env:
SKIP_FRONTEND_BUILD: "1"
# Preserve the server log even on failure so a red run doesn't
# require re-running locally to see what happened at boot.
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: bundled-binary-server-log
path: tests/bundled-binary/server.log
retention-days: 7
litmus: litmus:
name: WebDAV RFC 4918 — litmus (59/59) name: WebDAV RFC 4918 — litmus (59/59)
needs: build needs: build
+18 -5
View File
@@ -48,11 +48,24 @@ async-stream = "0.3.6"
async-trait = "0.1.89" async-trait = "0.1.89"
mime_guess = "2.0.5" mime_guess = "2.0.5"
# `rust-embed` — compile-time asset embedding for the `bundled-assets` feature. # `rust-embed` — compile-time asset embedding for the `bundled-assets` feature.
# Optional so default builds never pull it in. `compression` deflate-compresses # Optional so default builds never pull it in.
# each embedded file at compile time; the handler decompresses lazily on first #
# read (cached per-file in a `OnceCell`). See src/interfaces/web/embedded.rs # Features:
# for the actual embed struct + handlers. # * `compression` — deflate-compress each embedded file at compile time; the
rust-embed = { version = "8", features = ["compression", "include-exclude"], optional = true } # handler decompresses lazily on first read (cached per-file in a
# `OnceCell`). Halves the on-disk contribution to the final binary.
# * `include-exclude` — enables the `#[include]` / `#[exclude]` glob
# attributes on the derive.
# * `debug-embed` — CRITICAL. Without this, debug builds read files from
# disk at runtime (dynamic mode) instead of compiling them in. The
# runtime read is fragile (`CARGO_MANIFEST_DIR` resolution + working
# directory dependency) and returned `total=0` for us on 2026-08-28.
# With `debug-embed`, both `cargo build` and `cargo build --release`
# produce a truly self-contained binary — the only sensible default
# for the `bundled-assets` feature.
#
# See src/interfaces/web/embedded.rs for the actual embed struct + handlers.
rust-embed = { version = "8", features = ["compression", "include-exclude", "debug-embed"], optional = true }
uuid = { version = "1.23.3", features = ["v4", "v7", "serde"] } uuid = { version = "1.23.3", features = ["v4", "v7", "serde"] }
thiserror = "2.0.18" thiserror = "2.0.18"
arc-swap = "1.9" arc-swap = "1.9"
+3 -9
View File
@@ -45,15 +45,9 @@ fn bundled_assets_guard() {
// when cargo's default output is quiet; the panic below turns // when cargo's default output is quiet; the panic below turns
// them into a compile-time error so the missing prerequisite // them into a compile-time error so the missing prerequisite
// can't slip past a distracted dev. // can't slip past a distracted dev.
println!( println!("cargo:warning=`bundled-assets` feature requires static-dist/ at the repo root.");
"cargo:warning=`bundled-assets` feature requires static-dist/ at the repo root." println!("cargo:warning=Build the SvelteKit SPA first: (cd frontend && npm run build)");
); println!("cargo:warning=Or via the workspace shortcut: just fe-build");
println!(
"cargo:warning=Build the SvelteKit SPA first: (cd frontend && npm run build)"
);
println!(
"cargo:warning=Or via the workspace shortcut: just fe-build"
);
panic!( panic!(
"build.rs: missing {}/index.html — see the cargo:warning lines above", "build.rs: missing {}/index.html — see the cargo:warning lines above",
dist.display() dist.display()
+12 -6
View File
@@ -252,10 +252,10 @@ front-design:
# real browser, which the curl-driven # real browser, which the curl-driven
# suite above can't observe. # suite above can't observe.
# #
# Same chain runs in CI under the `api-test` job in # Same chain runs in CI under the `test-api` job in
# .github/workflows/ci.yml; keep the order in sync so a local pass means # .github/workflows/ci.yml; keep the order in sync so a local pass means
# CI passes. # CI passes.
api-test: test-api:
#!/usr/bin/env bash #!/usr/bin/env bash
set -x set -x
set -euo pipefail set -euo pipefail
@@ -270,6 +270,12 @@ api-test:
echo "XXX litmus webdav not found, ignore test" echo "XXX litmus webdav not found, ignore test"
fi fi
# backward compat
api-test: test-api
test-bundle:
./tests/bundled-binary/run.sh
# CalDAV client-driven conformance suite. # CalDAV client-driven conformance suite.
# #
# Drives OxiCloud through the maintained `python-caldav` client library # Drives OxiCloud through the maintained `python-caldav` client library
@@ -279,9 +285,9 @@ api-test:
# (RFC 5545 §3.8.4.4), and all-day masters (the shape #528 was filed # (RFC 5545 §3.8.4.4), and all-day masters (the shape #528 was filed
# against). # against).
# #
# Not chained into `api-test` because it needs python3; run explicitly. # Not chained into `test-api` because it needs python3; run explicitly.
# The orchestrator spawns its own postgres + server on port 8091 so it # The orchestrator spawns its own postgres + server on port 8091 so it
# can run in parallel with api-test/webdav. # can run in parallel with test-api/webdav.
# #
# Runs `cargo build` first so the orchestrator always sees a fresh # Runs `cargo build` first so the orchestrator always sees a fresh
# binary. run-pycaldav.sh itself doesn't rebuild — it uses whatever # binary. run-pycaldav.sh itself doesn't rebuild — it uses whatever
@@ -301,7 +307,7 @@ test-caldav:
# Manual, human-run: launches OxiCloud with OIDC as the ONLY login method # Manual, human-run: launches OxiCloud with OIDC as the ONLY login method
# (fake IdP on :1081, server on :8090) and waits for you to eyeball the # (fake IdP on :1081, server on :8090) and waits for you to eyeball the
# /login auto-redirect in a real browser. Not part of `just api-test` — # /login auto-redirect in a real browser. Not part of `just test-api` —
# there's no automated assertion here, it's a visual check. Ctrl-C to stop. # there's no automated assertion here, it's a visual check. Ctrl-C to stop.
#oidc-manual-sso-only: #oidc-manual-sso-only:
# bash tests/oidc/run-manual-sso-only.sh # bash tests/oidc/run-manual-sso-only.sh
@@ -383,4 +389,4 @@ test-docker-tags:
# Check and test everything # Check and test everything
# recommanded before pull request # recommanded before pull request
pre-pull-request: test-docker-tags check fe-check audit check-migrations test test-integration fe-test build api-test fe-build-e2e front-test pre-pull-request: test-docker-tags check fe-check audit check-migrations test test-integration fe-test build test-bundle test-api fe-build-e2e front-test
+6 -5
View File
@@ -108,9 +108,8 @@ fn asset_response(path: &str, bytes: std::borrow::Cow<'static, [u8]>) -> Respons
let mut resp = Response::new(Body::from(bytes.into_owned())); let mut resp = Response::new(Body::from(bytes.into_owned()));
resp.headers_mut().insert( resp.headers_mut().insert(
CONTENT_TYPE, CONTENT_TYPE,
HeaderValue::from_str(mime.as_ref()).unwrap_or(HeaderValue::from_static( HeaderValue::from_str(mime.as_ref())
"application/octet-stream", .unwrap_or(HeaderValue::from_static("application/octet-stream")),
)),
); );
resp resp
} }
@@ -119,8 +118,10 @@ fn spa_shell_response() -> Response {
match EmbeddedAssets::get("index.html") { match EmbeddedAssets::get("index.html") {
Some(shell) => { Some(shell) => {
let mut resp = Response::new(Body::from(shell.data.into_owned())); let mut resp = Response::new(Body::from(shell.data.into_owned()));
resp.headers_mut() resp.headers_mut().insert(
.insert(CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8")); CONTENT_TYPE,
HeaderValue::from_static("text/html; charset=utf-8"),
);
// Belt: the parent module also stamps this on unset, // Belt: the parent module also stamps this on unset,
// but stamp it here too so the shell never accidentally // but stamp it here too so the shell never accidentally
// ends up cacheable in front of a deploy. // ends up cacheable in front of a deploy.
+1 -4
View File
@@ -126,10 +126,7 @@ pub fn resolve_static_path(config: &AppConfig) -> PathBuf {
/// Caching: content-hashed assets under `/_app/immutable` are cached forever; /// Caching: content-hashed assets under `/_app/immutable` are cached forever;
/// everything else — crucially the `index.html` shell — is `no-cache` so a deploy /// everything else — crucially the `index.html` shell — is `no-cache` so a deploy
/// can't leave a stale app pinned in browsers. /// can't leave a stale app pinned in browsers.
pub fn create_web_routes( pub fn create_web_routes(app_state: Arc<AppState>, source: StaticSource) -> Router<Arc<AppState>> {
app_state: Arc<AppState>,
source: StaticSource,
) -> Router<Arc<AppState>> {
// `source` is resolved ONCE at boot in `main.rs::run()` and passed // `source` is resolved ONCE at boot in `main.rs::run()` and passed
// in — see the sequence there. Previously this fn called // in — see the sequence there. Previously this fn called
// `AppConfig::from_env()` + `resolve_static_source(&config)` itself // `AppConfig::from_env()` + `resolve_static_source(&config)` itself
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env bash
# Bundled-binary integration test.
#
# Builds `oxicloud` with `--features bundled-assets`, then boots it
# with the on-disk `static-dist/` moved aside and OXICLOUD_STATIC_PATH
# pointed at a nonexistent directory — the ONLY code path this can
# take is the embedded corpus. Then curls the SPA shell, a locale
# file, and a deep-link route to prove the embed serves correctly.
#
# Why this test exists: the bundled-assets feature has three failure
# modes that don't surface in normal filesystem-served CI:
#
# 1. rust-embed configuration bugs (glob patterns, `include`/`exclude`
# attrs). A wrong glob can silently produce a 0-file embed —
# caught 2026-08-28.
# 2. Debug-vs-release behaviour drift. rust-embed's dynamic-read mode
# in debug builds reads from disk at runtime, which masks embed
# bugs. `debug-embed` feature bakes files in for BOTH profiles
# (this test relies on it).
# 3. Axum `Path` extractor on fallback routes returning 500. The
# embedded `serve_root` handler needs `Request` extraction, not
# `Path` — caught 2026-08-28.
#
# All three are boot / first-request bugs a normal integration suite
# would miss. See docs/plan/bundled-binary.md § Verification.
#
# Usage (from repo root):
# bash tests/bundled-binary/run.sh
#
# Prerequisites: docker, cargo, node+npm (for the frontend build), curl
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
COMMON="$REPO_ROOT/tests/common"
TEST_DIR="$REPO_ROOT/tests/bundled-binary"
# shellcheck source=test.env
source "$TEST_DIR/test.env"
SERVER_PORT="${base_url##*:}"
log() { echo "[bundled-binary] $*"; }
die() { echo "[bundled-binary] ERROR: $*" >&2; exit 1; }
pass() { echo "[bundled-binary] ✓ $*"; }
fail() { echo "[bundled-binary] ✗ $*" >&2; FAILS=$((FAILS + 1)); }
wait_for_http() {
local url="$1" timeout="${2:-120}"
local deadline=$(( $(date +%s) + timeout ))
until curl -sf "$url" >/dev/null 2>&1; do
[[ $(date +%s) -ge $deadline ]] && die "Timeout waiting for $url"
sleep 1
done
}
# ── Cleanup state ────────────────────────────────────────────────────
#
# The trap kills the running server (a stray port-8090 process would
# collide with the next run) and tears down the test postgres. We do
# NOT touch `static-dist/` — the embed path is forced via
# `OXICLOUD_STATIC_PATH` alone (see step 5), so there's no filesystem
# state to restore.
SERVER_PID=""
FAILS=0
cleanup() {
local rc=$?
if [[ -n "$SERVER_PID" ]]; then
log "Stopping server (pid $SERVER_PID)..."
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
bash "$COMMON/stop-db.sh" 2>/dev/null || true
exit "$rc"
}
trap cleanup EXIT
# ── 1. Ensure static-dist/ is present at build time ─────────────────
#
# rust-embed's derive macro scans this directory at compile time. If
# it's missing, build.rs's `bundled_assets_guard` panics. This step
# builds the SPA in-place when the dir is absent.
#
# `SKIP_FRONTEND_BUILD=1` opts out and fails fast with a hint — useful
# in CI pipelines where the SPA build is a separate cached step
# upstream of this test.
if [[ ! -f "$REPO_ROOT/static-dist/index.html" ]]; then
if [[ "${SKIP_FRONTEND_BUILD:-0}" == "1" ]]; then
die "static-dist/index.html missing (SKIP_FRONTEND_BUILD=1). \
Build the SPA first: (cd frontend && npm ci && npm run build)"
fi
log "static-dist/ missing — building the SPA (set SKIP_FRONTEND_BUILD=1 to opt out)..."
if [[ ! -d "$REPO_ROOT/frontend/node_modules" ]]; then
log " running 'npm ci' (first-time install)..."
(cd "$REPO_ROOT/frontend" && npm ci) || die "npm ci failed"
fi
(cd "$REPO_ROOT/frontend" && npm run build) || die "npm run build failed"
[[ -f "$REPO_ROOT/static-dist/index.html" ]] || die "SPA build finished but static-dist/index.html still missing"
fi
# ── 2. Build with --features bundled-assets ──────────────────────────
#
# Debug build — matches what a dev iterates on locally, faster than
# --release, and the `debug-embed` feature in rust-embed makes debug
# and release behave identically here (both compile-time embed).
log "Building oxicloud with --features bundled-assets..."
(cd "$REPO_ROOT" && cargo build --features bundled-assets --bin oxicloud 2>&1 | tail -n 5) \
|| die "cargo build --features bundled-assets failed"
OXICLOUD_BIN="$REPO_ROOT/target/debug/oxicloud"
[[ -x "$OXICLOUD_BIN" ]] || die "Binary missing after build: $OXICLOUD_BIN"
# ── 3. Start test Postgres ───────────────────────────────────────────
log "Starting test Postgres via $COMMON/spawn-db.sh..."
bash "$COMMON/spawn-db.sh"
# ── 4. Boot the server with the embed forced ─────────────────────────
#
# `OXICLOUD_STATIC_PATH=/tmp/oxicloud-bundled-nonexistent-$$` is a path
# that provably doesn't exist (unique to this run's PID). The resolver
# in `resolve_static_source` runs two filesystem probes derived from
# this env var, BOTH of which miss:
# 1. `<parent>/static-dist/` → `/tmp/static-dist/` (vanishingly
# unlikely to exist)
# 2. `OXICLOUD_STATIC_PATH` itself — nonexistent by construction
# The resolver then falls through to `StaticSource::Embedded`. Repo-root
# `static-dist/` is never consulted at runtime — parenthood is derived
# from the env var, not CWD — so this test is stateless on the working
# directory.
STORAGE="$TEST_DIR/storage"
rm -rf "$STORAGE" && mkdir -p "$STORAGE"
LOG_FILE="$TEST_DIR/server.log"
: > "$LOG_FILE"
set -a
# shellcheck source=../common/server.env
source "$COMMON/server.env"
OXICLOUD_SERVER_PORT=$SERVER_PORT
OXICLOUD_STORAGE_PATH="$STORAGE"
OXICLOUD_STATIC_PATH="/tmp/oxicloud-bundled-nonexistent-$$"
# Disable the Prometheus /metrics listener — the shared server.env pins
# it to 127.0.0.1:9090 which collides when another test server (or a
# stray dev process) already holds that port, killing our boot before
# /ready is reachable. Metrics aren't part of what this test asserts.
OXICLOUD_METRICS_LISTEN=""
# Override the shared server.env's `RUST_LOG=warn,audit=info,...` which
# suppresses the info-level `static-assets:` lines this test asserts on
# (embed-source resolution + locale extraction). Match the app's own
# default (main.rs::run) so `http=warn` still tames the access log.
RUST_LOG="info,http=warn,http::web=error"
set +a
log "Starting server on port $SERVER_PORT with embed forced..."
"$OXICLOUD_BIN" > "$LOG_FILE" 2>&1 &
SERVER_PID=$!
wait_for_http "$base_url/ready" 120
log "Server ready — running assertions."
# ── 6. Assertions ────────────────────────────────────────────────────
# 6a. Boot log confirms the resolver picked the embed path (not
# silently fell back to a stale filesystem dir).
if grep -q 'static-assets: no filesystem source found, serving embedded corpus' "$LOG_FILE"; then
pass "boot log: resolver picked StaticSource::Embedded"
else
fail "boot log missing 'serving embedded corpus' line — was a filesystem source unexpectedly found?"
fi
# 6b. Boot log confirms N>0 locales were staged. This is the guard
# against the 2026-08-28 glob bug — a silent 0 would look like a
# "success" to a non-strict test.
if grep -Eq 'staged [1-9][0-9]* embedded locale file\(s\)' "$LOG_FILE"; then
staged=$(grep -oE 'staged [0-9]+ embedded locale' "$LOG_FILE" | tail -n1 | awk '{print $2}')
pass "boot log: staged $staged embedded locale file(s)"
else
fail "boot log: staged 0 locales (or line missing) ← the 2026-08-28 regression class"
fi
# 6c. SPA shell reachable at /
code=$(curl -s -o /dev/null -w '%{http_code}' "$base_url/")
if [[ "$code" == "200" ]]; then
pass "GET / → 200"
else
fail "GET / → $code (expected 200)"
fi
# 6d. Shell body looks like the SvelteKit-built index.html. Any of
# `<!doctype html>` or `data-color-scheme` or `<meta http-equiv=
# "content-security-policy"` would confirm it's the real shell
# and not an error page.
body=$(curl -s "$base_url/")
if echo "$body" | grep -qi '<!doctype html>' && echo "$body" | grep -q 'data-color-scheme'; then
pass "SPA shell body has SvelteKit markers (<!doctype html> + data-color-scheme)"
else
fail "SPA shell body doesn't look like the real index.html"
fi
# 6e. SPA fallback handler serves the shell for deep-link routes.
# `serve_root` MUST NOT return 500 here (the 2026-08-28 axum
# `Path` extractor bug on fallback routes).
for path in /login /files/some-deep-link; do
code=$(curl -s -o /dev/null -w '%{http_code}' "$base_url$path")
if [[ "$code" == "200" ]]; then
pass "GET $path → 200 (SPA fallback)"
else
fail "GET $path → $code (SPA fallback broken?)"
fi
done
# 6f. Favicon served from the embed (specific bytes, not the shell).
code=$(curl -s -o /dev/null -w '%{http_code}' "$base_url/favicon.ico")
ct=$(curl -sI "$base_url/favicon.ico" | grep -i '^content-type:' | tr -d '\r' | awk '{print $2}')
if [[ "$code" == "200" ]] && [[ "$ct" != text/html* ]]; then
pass "GET /favicon.ico → 200 with non-HTML content-type ($ct)"
else
fail "GET /favicon.ico → code=$code content-type=$ct (should be 200 image/*)"
fi
# 6g. Locale JSON reachable AND is valid JSON with expected shape.
locale_body=$(curl -sf "$base_url/locales/en.json" || echo '')
if echo "$locale_body" | head -c 1 | grep -q '{'; then
pass "GET /locales/en.json → JSON body"
else
fail "GET /locales/en.json didn't return a JSON body"
fi
# 6h. Immutable-asset cache header is applied by the `_app/immutable`
# nested router. Pick any hashed asset from the embed inventory
# — the boot log doesn't list them, so grep the shell HTML for one
# of its `modulepreload` refs.
imm_asset=$(echo "$body" | grep -oE '/_app/immutable/[^"]+\.js' | head -n1)
if [[ -n "$imm_asset" ]]; then
cc=$(curl -sI "$base_url$imm_asset" | grep -i '^cache-control:' | tr -d '\r')
if echo "$cc" | grep -q 'immutable'; then
pass "immutable-asset cache header applied: $cc"
else
fail "immutable-asset $imm_asset cache header wrong: $cc"
fi
else
fail "couldn't find a /_app/immutable/*.js reference in the shell HTML to test"
fi
# 6i. Shell HTML carries a Content-Security-Policy with sha256 script
# hashes. The SvelteKit build inlines a <meta http-equiv= ...> in
# index.html; that alone counts (belt). If the axum response-level
# CSP also carries hashes, even better (suspenders) — but the
# current middleware ships a hardcoded string, so we only check
# the meta tag which comes from the embedded bytes.
if echo "$body" | grep -q "'sha256-"; then
pass "SPA shell HTML carries CSP with sha256 script hashes (from Vite build)"
else
fail "SPA shell HTML has no sha256 CSP hashes — build produced a shell without them?"
fi
# ── Report ───────────────────────────────────────────────────────────
echo ""
if [[ $FAILS -gt 0 ]]; then
echo "─── SERVER LOG (last 60 lines) ───────────────────────────"
tail -n 60 "$LOG_FILE"
echo "──────────────────────────────────────────────────────────"
die "$FAILS assertion(s) failed"
fi
log "bundled-binary integration tests passed ✅"
+8
View File
@@ -0,0 +1,8 @@
# Bundled-binary integration test — runs the oxicloud server built with
# `--features bundled-assets` on a dedicated port so it doesn't collide
# with the api / webdav / oidc runners in a `just api-test` chain.
#
# Only variables the run.sh consumes as bash vars live here (test port,
# any curl-side seed data). Server-side env is loaded via
# tests/common/server.env inside the runner.
base_url=http://localhost:8090