#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" HOT_DEV_AUTH_LIB="${ROOT_DIR}/scripts/lib/hot-dev-auth.sh" if [[ ! -f "${HOT_DEV_AUTH_LIB}" ]]; then echo "hot-dev-auth.sh not found at ${HOT_DEV_AUTH_LIB}" >&2 exit 1 fi failures=0 temp_dirs=() cleanup() { local dir for dir in "${temp_dirs[@]:-}"; do rm -rf "${dir}" 2>/dev/null || true done } trap cleanup EXIT assert_contains() { local desc="$1" local haystack="$2" local needle="$3" if [[ "${haystack}" == *"${needle}"* ]]; then echo "[PASS] ${desc}" else echo "[FAIL] ${desc}" >&2 echo "Expected to find: ${needle}" >&2 ((failures++)) fi } make_temp_dir() { local dir dir="$(mktemp -d)" temp_dirs+=("${dir}") printf "%s\n" "${dir}" } test_default_auth_contract() { local output output="$( HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \ bash -lc ' source "${HOT_DEV_AUTH_LIB}" printf "user=%s\n" "$(hot_dev_resolve_auth_user)" printf "pass=%s\n" "$(hot_dev_resolve_auth_pass)" printf "banner=%s\n" "$(hot_dev_auth_banner_line "$(hot_dev_resolve_auth_user)" "$(hot_dev_resolve_auth_pass)")" ' )" assert_contains "default auth username stays admin" "${output}" "user=admin" assert_contains "default auth password stays adminadminadmin" "${output}" "banner=admin / adminadminadmin" assert_contains "default auth uses canonical bcrypt hash" "${output}" "pass=${HOT_DEV_DEFAULT_AUTH_HASH}" } test_custom_auth_banner_contract() { local output output="$( HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \ bash -lc ' source "${HOT_DEV_AUTH_LIB}" HOT_DEV_AUTH_USER="custom-admin" HOT_DEV_AUTH_PASS="custom-password" printf "banner=%s\n" "$(hot_dev_auth_banner_line "$(hot_dev_resolve_auth_user)" "$(hot_dev_resolve_auth_pass)")" ' )" assert_contains "custom auth banner hides the raw password" "${output}" "banner=custom via HOT_DEV_AUTH_USER / HOT_DEV_AUTH_PASS" } test_sync_auth_env_file_preserves_non_auth_settings() { local state_dir runtime_env output state_dir="$(make_temp_dir)" runtime_env="${state_dir}/.env" cat > "${runtime_env}" <<'EOF' # Auto-generated by Pulse Quick Security Setup PULSE_AUTH_USER='stale-user' PULSE_AUTH_PASS='stale-pass' PULSE_AUDIT_LOG=true PULSE_MOCK_MODE=false EOF output="$( HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \ RUNTIME_ENV_PATH="${runtime_env}" \ bash -lc ' source "${HOT_DEV_AUTH_LIB}" hot_dev_sync_auth_env_file "${RUNTIME_ENV_PATH}" "admin" "${HOT_DEV_DEFAULT_AUTH_HASH}" cat "${RUNTIME_ENV_PATH}" ' )" assert_contains "sync writes managed auth header" "${output}" "# Managed by hot-dev.sh for deterministic dev auth" assert_contains "sync rewrites auth user" "${output}" "PULSE_AUTH_USER='admin'" assert_contains "sync rewrites auth password hash" "${output}" "PULSE_AUTH_PASS='${HOT_DEV_DEFAULT_AUTH_HASH}'" assert_contains "sync preserves audit settings" "${output}" "PULSE_AUDIT_LOG=true" assert_contains "sync preserves mock settings" "${output}" "PULSE_MOCK_MODE=false" } test_sync_auth_env_file_removes_stale_temp_files() { local state_dir runtime_env output state_dir="$(make_temp_dir)" runtime_env="${state_dir}/.env" touch "${runtime_env}.tmp.111" "${runtime_env}.audit.222" output="$( HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \ RUNTIME_ENV_PATH="${runtime_env}" \ bash -lc ' source "${HOT_DEV_AUTH_LIB}" hot_dev_sync_auth_env_file "${RUNTIME_ENV_PATH}" "admin" "${HOT_DEV_DEFAULT_AUTH_HASH}" if compgen -G "${RUNTIME_ENV_PATH}.tmp.*" >/dev/null || compgen -G "${RUNTIME_ENV_PATH}.audit.*" >/dev/null; then printf "stale_temp_remaining=yes\n" else printf "stale_temp_remaining=no\n" fi ' )" assert_contains "sync removes stale managed temp files" "${output}" "stale_temp_remaining=no" } test_sync_auth_env_file_handles_managed_only_env_under_errexit() { local state_dir runtime_env output state_dir="$(make_temp_dir)" runtime_env="${state_dir}/.env" cat > "${runtime_env}" <<'EOF' # Managed by hot-dev.sh for deterministic dev auth PULSE_AUTH_USER='admin' PULSE_AUTH_PASS='stale-pass' EOF output="$( HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \ RUNTIME_ENV_PATH="${runtime_env}" \ bash -lc ' set -euo pipefail source "${HOT_DEV_AUTH_LIB}" hot_dev_sync_auth_env_file "${RUNTIME_ENV_PATH}" "admin" "${HOT_DEV_DEFAULT_AUTH_HASH}" printf "survived=yes\n" cat "${RUNTIME_ENV_PATH}" ' )" assert_contains "managed-only env does not trip errexit" "${output}" "survived=yes" assert_contains "managed-only sync keeps auth user" "${output}" "PULSE_AUTH_USER='admin'" assert_contains "managed-only sync rewrites auth password hash" "${output}" "PULSE_AUTH_PASS='${HOT_DEV_DEFAULT_AUTH_HASH}'" } test_sync_audit_signing_env_file_restores_missing_key() { local state_dir runtime_env output state_dir="$(make_temp_dir)" runtime_env="${state_dir}/.env" cat > "${runtime_env}" <<'EOF' # Managed by hot-dev.sh for deterministic dev auth PULSE_AUTH_USER='admin' PULSE_AUTH_PASS='hash' EOF output="$( HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \ RUNTIME_ENV_PATH="${runtime_env}" \ DATA_DIR="${state_dir}" \ bash -lc ' source "${HOT_DEV_AUTH_LIB}" hot_dev_sync_audit_signing_env_file "${RUNTIME_ENV_PATH}" "${DATA_DIR}" key_line_count="$(grep -c "^PULSE_AUDIT_SIGNING_KEY=" "${RUNTIME_ENV_PATH}")" key_value="$(grep "^PULSE_AUDIT_SIGNING_KEY=" "${RUNTIME_ENV_PATH}" | cut -d= -f2- | tr -d "\\047")" key_file_value="$(cat "${DATA_DIR}/.audit-signing.key")" mode="$(stat -c "%a" "${DATA_DIR}/.audit-signing.key" 2>/dev/null || stat -f "%Lp" "${DATA_DIR}/.audit-signing.key")" hot_dev_sync_audit_signing_env_file "${RUNTIME_ENV_PATH}" "${DATA_DIR}" key_line_count_after="$(grep -c "^PULSE_AUDIT_SIGNING_KEY=" "${RUNTIME_ENV_PATH}")" printf "key_line_count=%s\n" "${key_line_count}" printf "key_length=%s\n" "${#key_value}" printf "key_matches_file=%s\n" "$([[ "${key_value}" == "${key_file_value}" ]] && printf yes || printf no)" printf "key_line_count_after=%s\n" "${key_line_count_after}" printf "mode=%s\n" "${mode}" ' )" assert_contains "audit signing sync appends one env key" "${output}" "key_line_count=1" assert_contains "audit signing sync writes a 32-byte hex key" "${output}" "key_length=64" assert_contains "audit signing sync persists the same key to file" "${output}" "key_matches_file=yes" assert_contains "audit signing sync is idempotent" "${output}" "key_line_count_after=1" assert_contains "audit signing key file is private" "${output}" "mode=600" } source "${HOT_DEV_AUTH_LIB}" test_default_auth_contract test_custom_auth_banner_contract test_sync_auth_env_file_preserves_non_auth_settings test_sync_auth_env_file_removes_stale_temp_files test_sync_auth_env_file_handles_managed_only_env_under_errexit test_sync_audit_signing_env_file_restores_missing_key if (( failures > 0 )); then echo "FAIL: ${failures} hot-dev auth assertions failed" >&2 exit 1 fi echo "PASS: hot-dev auth contract checks passed"