diff --git a/scripts/test-start-sh-sync.sh b/scripts/test-start-sh-sync.sh new file mode 100755 index 0000000..f8d6eb3 --- /dev/null +++ b/scripts/test-start-sh-sync.sh @@ -0,0 +1,151 @@ +#!/bin/bash +# Regression test: start.sh dashboard bundle sync step. +# +# Validates the auto-sync that runs before `docker run` in start.sh: +# 1. Bundles are coppied from /opt/dashcaddy/status/dist → /var/www/dashcaddy-status/dist +# 2. sw.js is copied to /var/www/dashcaddy-status/sw.js +# 3. index.html is copied to /var/www/dashcaddy-status/index.html +# 4. Missing source dir is handled gracefully (WARN, no exit) +# 5. Sync is idempotent — re-running doesn't clobber or duplicate + +set -u +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FAILURES=0 + +pass() { echo " ✓ $1"; } +fail() { echo " ✗ $1"; FAILURES=$((FAILURES + 1)); } + +# -------------------------------------------------------------------------- +# Extract just the sync block from start.sh. The first call to `docker run` +# marks the end of the sync section. +# -------------------------------------------------------------------------- +extract_sync_block() { + awk ' + /^# Sync the freshly-built dashboard bundle/ { capture=1 } + /^docker run -d --restart unless-stopped/ { capture=0 } + capture { print } + ' "${SCRIPT_DIR}/../start.sh" +} + +fresh_workdir() { + local d; d="$(mktemp -d /tmp/dashcaddy-sync-test.XXXXXX)" + mkdir -p "$d/src/dist" + mkdir -p "$d/dst/dist" + echo "$d" +} + +cleanup() { rm -rf "$1"; } + +run_sync() { + local workdir="$1" + # Use absolute replacement paths that don't collide with sed prefixes. + # Inject a unique sentinel into start.sh paths first, then replace that + # sentinel with the test-specific paths. + local src_sentinel="__SYNC_TEST_SRC__" + local dst_sentinel="__SYNC_TEST_DST__" + local SRC_DIST_DIR="${workdir}/src/dist" + local DST_STATUS_DIR="${workdir}/dst" + + extract_sync_block | sed \ + -e "s|/opt/dashcaddy/status/dist|${src_sentinel}|g" \ + -e "s|/var/www/dashcaddy-status|${dst_sentinel}|g" \ + -e "s|${src_sentinel}|${SRC_DIST_DIR}|g" \ + -e "s|${dst_sentinel}|${DST_STATUS_DIR}|g" \ + > /tmp/_sync_block.sh + # shellcheck disable=SC1091 + source /tmp/_sync_block.sh +} + +# Test 1: Bundles + sw.js + index.html all get synced from a real source tree +echo "Test 1: fresh source → files synced to status dir" +WORKDIR="$(fresh_workdir)" +SRC="${WORKDIR}/src/dist" +echo 'console.log("core");' > "${SRC}/core.js" +echo 'console.log("features");' > "${SRC}/features.js" +echo 'self.addEventListener("fetch", ...)' > "${SRC}/sw.js" +echo 'hi' > "${SRC}/index.html" + +run_sync "${WORKDIR}" + +if [ -f "${WORKDIR}/dst/dist/core.js" ]; then + pass "core.js copied to status dir" +else + fail "core.js NOT copied" +fi +if [ -f "${WORKDIR}/dst/dist/features.js" ]; then + pass "features.js copied" +else + fail "features.js NOT copied" +fi +if [ -f "${WORKDIR}/dst/sw.js" ]; then + pass "sw.js copied (NOT under dist/)" +else + fail "sw.js NOT copied" +fi +if [ -f "${WORKDIR}/dst/index.html" ]; then + pass "index.html copied" +else + fail "index.html NOT copied" +fi +cleanup "${WORKDIR}" + +# Test 2: Missing source dir → WARN, no exit, no crash, no destinations created +echo +echo "Test 2: source dir missing → WARN, no exit" +WORKDIR="$(fresh_workdir)" +# SRC intentionally NOT created +run_sync "${WORKDIR}" +# If we reach here without an exit code propagating, we passed +if [ -z "$(ls -A "${WORKDIR}/dst/dist" 2>/dev/null)" ]; then + pass "no files copied when source missing" +else + fail "files copied when source was missing — should be no-op" +fi +cleanup "${WORKDIR}" + +# Test 3: Re-running syncs (idempotent — files overwritten with same content) +echo +echo "Test 3: idempotency (re-sync overwrites with same content)" +WORKDIR="$(fresh_workdir)" +SRC="${WORKDIR}/src/dist" +echo 'core-v1' > "${SRC}/core.js" +run_sync "${WORKDIR}" +echo 'core-v2' > "${SRC}/core.js" # mutate source +run_sync "${WORKDIR}" +if [ "$(cat "${WORKDIR}/dst/dist/core.js")" = "core-v2" ]; then + pass "second sync picked up the latest source" +else + fail "second sync did not update" +fi +cleanup "${WORKDIR}" + +# Test 4: set -e propagation — a single file-level failure doesn't take down the script +echo +echo "Test 4: set -e does not kill container start when sync hits a permission error" +WORKDIR="$(fresh_workdir)" +SRC="${WORKDIR}/src/dist" +echo 'core' > "${SRC}/core.js" +DST="${WORKDIR}/dst/dist" +chmod 555 "${DST}" # write-protected dest → cp -f would fail +# shellcheck disable=SC1091 +set -e +run_sync "${WORKDIR}" +EXIT=$? +set +e +chmod 755 "${DST}" 2>/dev/null +cleanup "${WORKDIR}" +if [ "$EXIT" -eq 0 ]; then + pass "sync step survives a permission-denied dest" +else + fail "set -e propagated a sync failure (exit ${EXIT}); would prevent container from starting" +fi + +rm -f /tmp/_sync_block.sh + +echo +if [ "$FAILURES" -eq 0 ]; then + echo "All sync regression tests passed." + exit 0 +fi +echo "${FAILURES} test(s) failed." +exit 1 diff --git a/start.sh b/start.sh index 0870b24..4d8484a 100755 --- a/start.sh +++ b/start.sh @@ -115,6 +115,26 @@ fi # that the local tailscaled handles; we never need to mutate state # from inside the container. echo "[start.sh] Creating container with full config..." + +# Sync the freshly-built dashboard bundle into the static directory Caddy +# serves. The Docker image bakes dist/ from the source tree at build time, +# but DNS2 also serves /var/www/dashcaddy-status/dist/ (the original +# Windows installer mirror path). If we don't sync after every build, the +# served bundle keeps the OLD hash while the API responds with new code, +# which shows up in the dashboard as "version unavailable" + "no data" +# widgets because the new API surface doesn't match the old widget code. +# This step is idempotent and ~50ms — always safe to run. +echo "[start.sh] Syncing dashboard bundle into static dir..." +mkdir -p /var/www/dashcaddy-status/dist +if [ -d /opt/dashcaddy/status/dist ]; then + cp /opt/dashcaddy/status/dist/*.js /var/www/dashcaddy-status/dist/ 2>/dev/null || true + cp /opt/dashcaddy/status/sw.js /var/www/dashcaddy-status/ 2>/dev/null || true + cp /opt/dashcaddy/status/index.html /var/www/dashcaddy-status/ 2>/dev/null || true + echo "[start.sh] Bundle synced ($(ls /opt/dashcaddy/status/dist/*.js 2>/dev/null | wc -l) bundle files + sw.js + index.html)." +else + echo "[start.sh] WARN: /opt/dashcaddy/status/dist missing — skipping sync (frontend will be stale)." +fi + docker run -d --restart unless-stopped --name ${CONTAINER_NAME} \ --add-host=get.dashcaddy.net:194.233.88.206 \ --add-host=get2.dashcaddy.net:194.233.88.206 \