mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:47:02 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
202 lines
8.7 KiB
Bash
Executable File
202 lines
8.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Tests for website_admin.sh — the personal website's browser post editor.
|
|
#
|
|
# The subject is driven for real, including the sudo-write of the systemd unit,
|
|
# which the jail bind-mounts to a throwaway /etc. Only two things are stood in
|
|
# for: `pnpm`, because building the editor is the website repo's own test
|
|
# suite's job, and `admin_cli`, because hashing a password with scrypt costs
|
|
# ~100 ms per call and proves nothing here.
|
|
#
|
|
# What IS asserted is the content: a unit whose PATH lacks nvm's node, or an
|
|
# env file that lost PW_ADMIN_ROOT, starts cleanly and then fails on the first
|
|
# save — the exact failure a presence check would pass.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=./features_harness.sh
|
|
. "$SCRIPT_DIR/features_harness.sh"
|
|
|
|
# --- Refuse to run outside the jail -----------------------------------------
|
|
if [[ -d /etc/systemd/system ]] && [[ ! -w /etc/systemd/system ]]; then
|
|
printf 'REFUSING: this suite must run under shell_coverage_jail.sh\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
_t_setup_env
|
|
trap _t_teardown EXIT
|
|
|
|
# The globals website_admin.sh inherits from setup_personal_website.sh above
|
|
# its source line, plus the log helpers it takes from common.sh.
|
|
WEBSITE_SRC="$TEST_TMPDIR/website"
|
|
WEBSITE_DATA_DIR="$TEST_TMPDIR/serve"
|
|
mkdir -p "$WEBSITE_SRC/dist-admin/client"
|
|
|
|
log_info() { printf '[info] %s\n' "$1"; }
|
|
log_ok() { printf '[ok] %s\n' "$1"; }
|
|
log_warn() { printf '[warn] %s\n' "$1" >&2; }
|
|
log_error() { printf '[error] %s\n' "$1" >&2; }
|
|
die() {
|
|
log_error "$1"
|
|
exit 1
|
|
}
|
|
ensure_dir() { mkdir -p "$1"; }
|
|
status_line() { printf '[status %s] %s\n' "$1" "$2"; }
|
|
check_http() { echo 1; }
|
|
|
|
# shellcheck source=../website_admin.sh
|
|
. "$FEATURES_LIB_DIR/website_admin.sh"
|
|
|
|
# Shadow sudo for this suite only. The jail already runs as uid 0, so `sudo`
|
|
# adds nothing here -- but it resets PATH, which would send
|
|
# `sudo systemctl daemon-reload` to the real binary and reload the HOST's
|
|
# systemd, one of the few things the namespace does not contain.
|
|
sudo() { "$@"; }
|
|
|
|
# A stand-in scrypt hash. Escaped inside double quotes rather than written in
|
|
# single quotes: the format is $-separated, and this repo treats shellcheck's
|
|
# SC2016 as an error.
|
|
readonly FAKE_HASH="scrypt\$32768\$8\$1\$deadbeef\$cafe"
|
|
|
|
# Stand in for the service's own CLI: deterministic, and 200x faster.
|
|
admin_cli() {
|
|
case "$1" in
|
|
hash-password) printf '%s\n' "$FAKE_HASH" ;;
|
|
session-secret) printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# --- admin_new_password -----------------------------------------------------
|
|
password="$(admin_new_password)"
|
|
if [[ ${#password} -ge 16 ]]; then
|
|
_t_pass "admin_new_password returns something long enough to be a password"
|
|
else
|
|
_t_fail "admin_new_password returned only ${#password} characters"
|
|
fi
|
|
# `/` and `+` would survive a shell round trip badly and `=` reads as padding
|
|
# someone will trim; the generator strips all three rather than hoping.
|
|
if [[ $password =~ ^[A-Za-z0-9]+$ ]]; then
|
|
_t_pass "admin_new_password stays URL- and copy-paste-safe"
|
|
else
|
|
_t_fail "admin_new_password produced '${password}'"
|
|
fi
|
|
if [[ "$(admin_new_password)" == "$(admin_new_password)" ]]; then
|
|
_t_fail "admin_new_password returned the same value twice"
|
|
else
|
|
_t_pass "admin_new_password draws fresh entropy each call"
|
|
fi
|
|
|
|
# --- admin_env_text ---------------------------------------------------------
|
|
env_text="$(admin_env_text "$FAKE_HASH" 'the-secret')"
|
|
_t_has "$env_text" "PW_ADMIN_PASSWORD_HASH=${FAKE_HASH}" "the env file carries the hash"
|
|
_t_has "$env_text" 'PW_ADMIN_SESSION_SECRET=the-secret' "the env file carries the session secret"
|
|
_t_has "$env_text" 'PW_ADMIN_HOST=127.0.0.1' "the service is pinned to loopback"
|
|
_t_has "$env_text" "PW_ADMIN_ROOT=${WEBSITE_SRC}" "the content root points at the checkout"
|
|
|
|
# --- ensure_admin_secrets ---------------------------------------------------
|
|
ensure_admin_secrets
|
|
_t_file_has "$ADMIN_ENV_FILE" 'PW_ADMIN_PASSWORD_HASH=scrypt' "the env file is written"
|
|
_t_eq "600" "$(stat -c '%a' "$ADMIN_ENV_FILE")" "the env file is not world-readable"
|
|
_t_eq "600" "$(stat -c '%a' "$ADMIN_PASSWORD_FILE")" "the password file is not world-readable"
|
|
|
|
# The password must never be recoverable from the file the unit reads.
|
|
stored_password="$(cat "$ADMIN_PASSWORD_FILE")"
|
|
if grep -qF "$stored_password" "$ADMIN_ENV_FILE"; then
|
|
_t_fail "the env file contains the plaintext password"
|
|
else
|
|
_t_pass "the env file holds only the hash, never the password"
|
|
fi
|
|
|
|
# Re-running setup must not rotate a password someone is already using.
|
|
ensure_admin_secrets
|
|
_t_eq "$stored_password" "$(cat "$ADMIN_PASSWORD_FILE")" \
|
|
"ensure_admin_secrets leaves existing credentials alone"
|
|
|
|
# A truncated env file is not credentials, and must be regenerated rather than
|
|
# reused into a service that cannot authenticate anyone.
|
|
printf 'PW_ADMIN_PORT=4321\n' >"$ADMIN_ENV_FILE"
|
|
ensure_admin_secrets
|
|
_t_file_has "$ADMIN_ENV_FILE" 'PW_ADMIN_PASSWORD_HASH=scrypt' \
|
|
"a hashless env file is rewritten instead of kept"
|
|
|
|
# --- admin_unit_text --------------------------------------------------------
|
|
unit_text="$(admin_unit_text)"
|
|
node_dir="$(dirname "$(command -v node)")"
|
|
_t_has "$unit_text" "Environment=PATH=${node_dir}:" \
|
|
"the unit puts this node on PATH, so the build steps can shell out to it"
|
|
_t_has "$unit_text" "WorkingDirectory=${WEBSITE_SRC}" "the unit runs from the checkout"
|
|
_t_has "$unit_text" "EnvironmentFile=${ADMIN_ENV_FILE}" "the unit reads the generated env file"
|
|
_t_has "$unit_text" "ReadWritePaths=${WEBSITE_SRC}" \
|
|
"ProtectSystem=strict is opened up for exactly the checkout"
|
|
_t_has "$unit_text" "NoNewPrivileges=yes" "the unit drops privilege escalation"
|
|
_t_has "$unit_text" "Restart=always" "the unit comes back after a crash or reboot"
|
|
if [[ $unit_text == *"${WEBSITE_SRC}/dist-admin/admin.js"* ]]; then
|
|
_t_pass "the unit starts the built service, not a source file"
|
|
else
|
|
_t_fail "the unit does not start dist-admin/admin.js"
|
|
fi
|
|
|
|
# --- write_admin_unit -------------------------------------------------------
|
|
_t_stub systemctl
|
|
write_admin_unit
|
|
_t_file_has "$ADMIN_UNIT_PATH" "ExecStart=" "the unit lands in /etc/systemd/system"
|
|
_t_called "daemon-reload" "write_admin_unit reloads systemd so the new unit is seen"
|
|
|
|
# --- admin_report -----------------------------------------------------------
|
|
report="$(admin_report)"
|
|
_t_has "$report" "-L ${ADMIN_PORT}:127.0.0.1:${ADMIN_PORT}" "the report gives the tunnel command"
|
|
_t_has "$report" "http://localhost:${ADMIN_PORT}/admin" \
|
|
"the report sends you to localhost, which is where the Secure cookie works"
|
|
_t_has "$report" "$ADMIN_PASSWORD_FILE" "the report says where the password is"
|
|
if [[ $report == *"$stored_password"* ]]; then
|
|
_t_fail "the report prints the password into the terminal and the setup log"
|
|
else
|
|
_t_pass "the report names the password file rather than printing the password"
|
|
fi
|
|
|
|
# --- build_website_admin ----------------------------------------------------
|
|
_t_stub pnpm
|
|
# pnpm is stubbed, so nothing is produced: the build must notice rather than
|
|
# hand a missing bundle to systemd.
|
|
_t_exits_nonzero build_website_admin "build_website_admin fails when no bundle was produced"
|
|
|
|
printf 'built\n' >"${WEBSITE_SRC}/dist-admin/admin.js"
|
|
_t_exits_nonzero build_website_admin \
|
|
"build_website_admin fails when the service built but the editor page did not"
|
|
|
|
printf '<html></html>\n' >"${WEBSITE_SRC}/dist-admin/client/index.html"
|
|
if build_website_admin >/dev/null; then
|
|
_t_pass "build_website_admin passes once both halves exist"
|
|
else
|
|
_t_fail "build_website_admin rejected a complete build"
|
|
fi
|
|
|
|
# --- start_admin_service ----------------------------------------------------
|
|
# A curl that answers 200 stands in for a service that came up. The opposite
|
|
# case is deliberately not exercised here: wait_for_admin retries for 20 s
|
|
# before giving up, and a 20 s test is one nobody will keep running.
|
|
printf '#!/usr/bin/env bash\nprintf 200\n' >"$TEST_TMPDIR/bin/curl"
|
|
chmod +x "$TEST_TMPDIR/bin/curl"
|
|
if start_admin_service >/dev/null 2>&1; then
|
|
_t_pass "start_admin_service accepts a service that answers 200 on /admin"
|
|
else
|
|
_t_fail "start_admin_service rejected a healthy service"
|
|
fi
|
|
_t_called "enable --now ${ADMIN_UNIT}" "start_admin_service enables the unit for reboot survival"
|
|
|
|
# --- deploy_admin_service ---------------------------------------------------
|
|
if deploy_admin_service >/dev/null 2>&1; then
|
|
_t_pass "deploy_admin_service runs build, secrets, unit and start in one pass"
|
|
else
|
|
_t_fail "deploy_admin_service failed on an otherwise healthy setup"
|
|
fi
|
|
|
|
# --- admin_status_lines -----------------------------------------------------
|
|
if admin_status_lines >/dev/null 2>&1; then
|
|
_t_pass "admin_status_lines reports without failing the status command"
|
|
else
|
|
_t_fail "admin_status_lines exited non-zero"
|
|
fi
|
|
|
|
_t_report "test_website_admin.sh"
|