#!/bin/bash # DC-039 follow-up — regression test for start.sh image-layer migration step. # Validates: idempotency, partial files, missing files, sentinel creation, # set -e doesn't kill the script on a single per-file failure. set -u SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FAILURES=0 pass() { echo " ✓ $1"; } fail() { echo " ✗ $1"; FAILURES=$((FAILURES + 1)); } # ---- Setup helpers ---------------------------------------------------------- # Source only the migration function out of start.sh — don't run the whole # script (it would try to bind to port 3001 + manage docker). Use the same # sh-extraction pattern as test-dashcaddy-update-backup.sh. fresh_data_dir() { local d d="$(mktemp -d /tmp/dashcaddy-migration-test.XXXXXX)" echo "${d}" } clean_data_dir() { rm -rf "$1" 2>/dev/null || true } # Extract just the migration logic — it's the only block we want to test. extract_migration() { sed -n '/^MIGRATION_SENTINEL=/,/^run_image_layer_migration$/p' "${SCRIPT_DIR}/../start.sh" } # Test 1: Sentinel file present → migration skips entirely echo "Test 1: sentinel exists → no copies" DATA_DIR="$(fresh_data_dir)" touch "${DATA_DIR}/.migrated-from-image-layer" extract_migration > /tmp/_migration_extract.sh # Override DATA_DIR to point at our test dir # Strip the actual call (the trailing 'run_image_layer_migration') so we # control invocation; in tests we re-define DATA_DIR first. { echo "DATA_DIR='${DATA_DIR}'" echo "MIGRATION_SENTINEL=\"\${DATA_DIR}/.migrated-from-image-layer\"" sed -n '/^IMAGE_LAYER_ZOMBIES=(/,/^)$/p' "${SCRIPT_DIR}/../start.sh" sed -n '/^run_image_layer_migration()/,/^}$/p' "${SCRIPT_DIR}/../start.sh" } > /tmp/_migration_block.sh # shellcheck disable=SC1091 source /tmp/_migration_block.sh # Plant a fake zombie that should NOT be migrated because the sentinel exists ZOMBIE_DIR="$(mktemp -d /tmp/dashcaddy-zombie.XXXXXX)" mkdir -p "${ZOMBIE_DIR}/security" echo '{"data":"should not be migrated"}' > "${ZOMBIE_DIR}/security/audit-log.json" run_image_layer_migration if [ -f "${DATA_DIR}/migrated-audit-log.json" ]; then fail "test 1: sentinel existed, migration should have skipped but a file appeared" else pass "sentinel skipped migration cleanly" fi rm -rf "${ZOMBIE_DIR}" clean_data_dir "${DATA_DIR}" # Test 2: No sentinel + non-empty zombie → migration copies file echo "Test 2: zombie file present → migration copies to bind mount" DATA_DIR="$(fresh_data_dir)" ZOMBIE_DIR="$(mktemp -d /tmp/dashcaddy-zombie.XXXXXX)" mkdir -p "${ZOMBIE_DIR}/security" "${ZOMBIE_DIR}/managers" echo '{"audit":"prod data"}' > "${ZOMBIE_DIR}/security/audit-log.json" echo "license-secret-blob" > "${ZOMBIE_DIR}/managers/.license-secret" { echo "DATA_DIR='${DATA_DIR}'" echo "MIGRATION_SENTINEL=\"\${DATA_DIR}/.migrated-from-image-layer\"" sed -n '/^IMAGE_LAYER_ZOMBIES=(/,/^)$/p' "${SCRIPT_DIR}/../start.sh" sed -n '/^run_image_layer_migration()/,/^}$/p' "${SCRIPT_DIR}/../start.sh" } > /tmp/_migration_block2.sh # Stub out the real zombie paths to point at our temp zombie sed -i "s|/opt/dashcaddy/dashcaddy-api/src/security/audit-log.json|${ZOMBIE_DIR}/security/audit-log.json|g" /tmp/_migration_block2.sh sed -i "s|/opt/dashcaddy/dashcaddy-api/src/managers/.license-secret|${ZOMBIE_DIR}/managers/.license-secret|g" /tmp/_migration_block2.sh # shellcheck disable=SC1091 source /tmp/_migration_block2.sh run_image_layer_migration if [ ! -f "${DATA_DIR}/migrated-audit-log.json" ]; then fail "test 2: audit-log.json not migrated" elif ! grep -q "audit.*prod data" "${DATA_DIR}/migrated-audit-log.json"; then fail "test 2: audit-log.json migrated but content corrupt" else pass "audit-log.json migrated with correct content" fi if [ ! -f "${DATA_DIR}/migrated-.license-secret" ]; then fail "test 2: .license-secret not migrated" else pass ".license-secret migrated" fi if [ ! -f "${DATA_DIR}/.migrated-from-image-layer" ]; then fail "test 2: sentinel file was not written" else pass "sentinel file written" fi rm -rf "${ZOMBIE_DIR}" clean_data_dir "${DATA_DIR}" # Test 3: Idempotency — running migration twice does NOT clobber first copy echo "Test 3: idempotency" DATA_DIR="$(fresh_data_dir)" ZOMBIE_DIR="$(mktemp -d /tmp/dashcaddy-zombie.XXXXXX)" mkdir -p "${ZOMBIE_DIR}/security" echo '{"first":true}' > "${ZOMBIE_DIR}/security/audit-log.json" { echo "DATA_DIR='${DATA_DIR}'" echo "MIGRATION_SENTINEL=\"\${DATA_DIR}/.migrated-from-image-layer\"" sed -n '/^IMAGE_LAYER_ZOMBIES=(/,/^)$/p' "${SCRIPT_DIR}/../start.sh" sed -n '/^run_image_layer_migration()/,/^}$/p' "${SCRIPT_DIR}/../start.sh" } > /tmp/_migration_block3.sh sed -i "s|/opt/dashcaddy/dashcaddy-api/src/security/audit-log.json|${ZOMBIE_DIR}/security/audit-log.json|g" /tmp/_migration_block3.sh # shellcheck disable=SC1091 source /tmp/_migration_block3.sh run_image_layer_migration echo '{"second":true}' > "${ZOMBIE_DIR}/security/audit-log.json" # mutate the source after migration run_image_layer_migration if grep -q "first.*true" "${DATA_DIR}/migrated-audit-log.json"; then pass "second run did not overwrite first migrated content" else fail "second run overwrote the migrated file" fi rm -rf "${ZOMBIE_DIR}" clean_data_dir "${DATA_DIR}" # Test 4: Zero-byte zombie (empty file) → NOT migrated echo "Test 4: empty file is not migrated" DATA_DIR="$(fresh_data_dir)" ZOMBIE_DIR="$(mktemp -d /tmp/dashcaddy-zombie.XXXXXX)" mkdir -p "${ZOMBIE_DIR}/security" touch "${ZOMBIE_DIR}/security/audit-log.json" # zero bytes { echo "DATA_DIR='${DATA_DIR}'" echo "MIGRATION_SENTINEL=\"\${DATA_DIR}/.migrated-from-image-layer\"" sed -n '/^IMAGE_LAYER_ZOMBIES=(/,/^)$/p' "${SCRIPT_DIR}/../start.sh" sed -n '/^run_image_layer_migration()/,/^}$/p' "${SCRIPT_DIR}/../start.sh" } > /tmp/_migration_block4.sh sed -i "s|/opt/dashcaddy/dashcaddy-api/src/security/audit-log.json|${ZOMBIE_DIR}/security/audit-log.json|g" /tmp/_migration_block4.sh # shellcheck disable=SC1091 source /tmp/_migration_block4.sh run_image_layer_migration if [ -f "${DATA_DIR}/migrated-audit-log.json" ]; then fail "test 4: empty file should not be migrated" else pass "empty file correctly skipped" fi if [ -f "${DATA_DIR}/.migrated-from-image-layer" ]; then pass "sentinel still written even with zero zombies" else fail "sentinel should still be written even with no zombies" fi rm -rf "${ZOMBIE_DIR}" clean_data_dir "${DATA_DIR}" # Test 5: set -e present + all per-file failures → script doesn't take down container echo "Test 5: a single per-file failure does not bring down the container" DATA_DIR="$(fresh_data_dir)" ZOMBIE_DIR="$(mktemp -d /tmp/dashcaddy-zombie.XXXXXX)" mkdir -p "${ZOMBIE_DIR}/security" echo "x" > "${ZOMBIE_DIR}/security/audit-log.json" chmod 000 "${ZOMBIE_DIR}/security/audit-log.json" # make it unreadable so cp -a fails { set -e # NOW we need to verify the inner guard prevents set -e from killing us echo "DATA_DIR='${DATA_DIR}'" echo "MIGRATION_SENTINEL=\"\${DATA_DIR}/.migrated-from-image-layer\"" sed -n '/^IMAGE_LAYER_ZOMBIES=(/,/^)$/p' "${SCRIPT_DIR}/../start.sh" sed -n '/^run_image_layer_migration()/,/^}$/p' "${SCRIPT_DIR}/../start.sh" } > /tmp/_migration_block5.sh sed -i "s|/opt/dashcaddy/dashcaddy-api/src/security/audit-log.json|${ZOMBIE_DIR}/security/audit-log.json|g" /tmp/_migration_block5.sh EXIT=0 # shellcheck disable=SC1091 source /tmp/_migration_block5.sh && run_image_layer_migration || EXIT=$? chmod 644 "${ZOMBIE_DIR}/security/audit-log.json" 2>/dev/null || true rm -rf "${ZOMBIE_DIR}" clean_data_dir "${DATA_DIR}" if [ "$EXIT" -eq 0 ]; then pass "script survived a per-file cp failure" else fail "set -e propagated a per-file failure (exit ${EXIT}); container would not boot" fi # ---- Cleanup ---------------------------------------------------------------- rm -f /tmp/_migration_extract.sh /tmp/_migration_block*.sh echo if [ "$FAILURES" -eq 0 ]; then echo "All migration regression tests passed." exit 0 fi echo "${FAILURES} test(s) failed." exit 1