#!/usr/bin/env bash # Tests for the installer's Node provisioning. # # The installer's whole promise is that one command turns a bare host into a working # one. Node was the exception: it was a hard prerequisite the installer checked and # refused, so on a greenfield host the documented one-command install failed first. # These tests pin the fixed behaviour, including the refusals. # # Everything runs offline. MOSAIC_NODE_DIST points at a local directory laid out like # nodejs.org/dist, served over file:// -- so the download, the checksum gate, and the # unpack are the real code paths, with no network and no real Node download. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-node-provision-test-XXXXXX")" trap 'rm -rf "$TMP"' EXIT DIST="$TMP/dist" FAKE_BIN="$TMP/bin" HOME_DIR="$TMP/home" PREFIX="$TMP/prefix" MOSAIC_HOME_DIR="$TMP/mosaic" STATE="$TMP/state" LOG="$TMP/npm.log" NODE_HOME="$TMP/nodehome" mkdir -p "$DIST" "$FAKE_BIN" "$HOME_DIR" "$STATE" REAL_NODE="$(command -v node)" # The platform triple, derived the same way the installer derives it. case "$(uname -s)" in Linux) TEST_OS=linux ;; Darwin) TEST_OS=darwin ;; *) echo "[skip] no Node build for $(uname -s)"; exit 0 ;; esac case "$(uname -m)" in x86_64|amd64) TEST_ARCH=x64 ;; aarch64|arm64) TEST_ARCH=arm64 ;; armv7l) TEST_ARCH=armv7l ;; *) echo "[skip] no Node build for $(uname -m)"; exit 0 ;; esac PLATFORM="${TEST_OS}-${TEST_ARCH}" VERSION=v22.99.0 OLD_VERSION=v20.99.0 # ─── fixtures ───────────────────────────────────────────────────────────────── # A node stub that answers the installer's version probe and defers everything else # to the real interpreter, so the rest of the install still runs. # # The major is baked in per stub rather than read from the environment. A shared env # var would be read by the downloaded Node too, so the "system Node is too old" case # would install a replacement that also claimed to be too old. write_node_stub() { local path="$1" major="${2:-22}" cat > "$path" < "$1" <<'STUB' #!/usr/bin/env bash set -euo pipefail echo "$*" >> "${MOSAIC_TEST_NPM_LOG:?}" STATE="${MOSAIC_TEST_STATE:?}" if [[ "${1:-}" == "view" ]]; then case "$2 $3" in "@mosaicstack/mosaic@next version") echo "0.0.50-next.999" ;; "@mosaicstack/gateway@next version") echo "0.0.7-next.999" ;; "@mosaicstack/mosaic version") echo "0.0.49" ;; *) echo "unexpected npm view: $*" >&2; exit 1 ;; esac exit 0 fi if [[ "${1:-}" == "install" ]]; then case "$*" in *"@mosaicstack/mosaic@"*) echo "0.0.50-next.999" > "$STATE/mosaic" ;; *"@mosaicstack/gateway@"*) echo "0.0.7-next.999" > "$STATE/gateway" ;; esac exit 0 fi if [[ "${1:-}" == "ls" ]]; then printf '{"dependencies":{"@mosaicstack/mosaic":{"version":"%s"},"@mosaicstack/gateway":{"version":"%s"}}}\n' \ "$(cat "$STATE/mosaic" 2>/dev/null || echo '')" \ "$(cat "$STATE/gateway" 2>/dev/null || echo '')" exit 0 fi exit 0 STUB chmod +x "$1" } # Build a nodejs.org-shaped release: the tarball, and a SHASUMS256.txt over it. publish_release() { local version="$1" corrupt_checksum="${2:-false}" local base="node-${version}-${PLATFORM}" local stage="$TMP/stage-${version}" rm -rf "$stage" mkdir -p "$stage/${base}/bin" write_node_stub "$stage/${base}/bin/node" "$(sed 's/^v//; s/\..*//' <<<"$version")" write_npm_stub "$stage/${base}/bin/npm" mkdir -p "${DIST}/${version}" tar -czf "${DIST}/${version}/${base}.tar.gz" -C "$stage" "$base" local sum if command -v sha256sum &>/dev/null; then sum="$(sha256sum "${DIST}/${version}/${base}.tar.gz" | awk '{print $1}')" else sum="$(shasum -a 256 "${DIST}/${version}/${base}.tar.gz" | awk '{print $1}')" fi if [[ "$corrupt_checksum" == "true" ]]; then sum="0000000000000000000000000000000000000000000000000000000000000000" fi printf '%s %s.tar.gz\n' "$sum" "$base" > "${DIST}/${version}/SHASUMS256.txt" } publish_release "$VERSION" publish_release "$OLD_VERSION" # Newest-first, as nodejs.org publishes it. printf '[{"version":"%s"},{"version":"%s"}]\n' "$VERSION" "$OLD_VERSION" > "$DIST/index.json" # A PATH with the usual tools but no Node toolchain, so "a host with no Node" is # actually true on a developer machine and in CI, both of which have one installed. NONODE_BIN="$TMP/nonode-bin" mkdir -p "$NONODE_BIN" for candidate in /usr/bin/* /bin/*; do [[ -e "$candidate" ]] || continue case "$(basename "$candidate")" in node|npm|npx|corepack|nodejs) continue ;; esac ln -sf "$candidate" "$NONODE_BIN/$(basename "$candidate")" 2>/dev/null || true done if PATH="$NONODE_BIN" command -v node &>/dev/null; then echo "[skip] could not build a Node-free PATH on this host" >&2 exit 0 fi reset_home() { rm -rf "$HOME_DIR" "$PREFIX" "$MOSAIC_HOME_DIR" "$NODE_HOME" "$LOG" "$STATE" mkdir -p "$HOME_DIR" "$STATE" : > "$LOG" } # Run the installer with no Node anywhere on PATH. run_bare() { env -u npm_config_prefix \ HOME="$HOME_DIR" \ MOSAIC_HOME="$MOSAIC_HOME_DIR" \ MOSAIC_PREFIX="$PREFIX" \ MOSAIC_NO_COLOR=1 \ MOSAIC_NODE_HOME="$NODE_HOME" \ MOSAIC_NODE_DIST="file://${DIST}" \ MOSAIC_TEST_REAL_NODE="$REAL_NODE" \ MOSAIC_TEST_NPM_LOG="$LOG" \ MOSAIC_TEST_STATE="$STATE" \ PATH="$NONODE_BIN" \ bash "$ROOT/tools/install.sh" "$@" } # ─── tests ──────────────────────────────────────────────────────────────────── reset_home echo "[test] a host with no Node gets one, and the CLI install proceeds" OUTPUT="$(run_bare --cli --next --yes --no-auto-launch 2>&1)" grep -qF -- "Node is not installed" <<<"$OUTPUT" grep -qF -- "Installed Node ${VERSION}" <<<"$OUTPUT" [[ -x "${NODE_HOME}/${VERSION}/bin/node" ]] grep -qF -- "install -g @mosaicstack/mosaic@0.0.50-next.999" "$LOG" echo "[test] the newest matching major is chosen, not merely the first published" [[ ! -d "${NODE_HOME}/${OLD_VERSION}" ]] echo "[test] future shells can find both Node and the CLI" grep -qF -- "export PATH=\"${NODE_HOME}/${VERSION}/bin:\$PATH\"" "$HOME_DIR/.profile" grep -qF -- "export PATH=\"${PREFIX}/bin:\$PATH\"" "$HOME_DIR/.profile" # Debian's .bashrc returns early when non-interactive, so the login profile is the # one that matters -- but an interactive non-login shell only reads .bashrc. grep -qF -- "export PATH=\"${NODE_HOME}/${VERSION}/bin:\$PATH\"" "$HOME_DIR/.bashrc" grep -qF -- "export PATH=\"${PREFIX}/bin:\$PATH\"" "$HOME_DIR/.bashrc" echo "[test] re-running reuses the Node it installed and does not duplicate PATH lines" OUTPUT="$(run_bare --cli --next --yes --no-auto-launch 2>&1)" grep -qF -- "from ${NODE_HOME}" <<<"$OUTPUT" [[ "$(grep -c 'export PATH=' "$HOME_DIR/.profile")" -eq 2 ]] [[ "$(grep -c 'export PATH=' "$HOME_DIR/.bashrc")" -eq 2 ]] reset_home echo "[test] --no-node-install refuses instead of installing" set +e OUTPUT="$(run_bare --cli --next --yes --no-node-install 2>&1)" RC=$? set -e [[ "$RC" -ne 0 ]] grep -qF -- "--no-node-install was given" <<<"$OUTPUT" [[ ! -d "$NODE_HOME" ]] reset_home echo "[test] --check never provisions Node" set +e OUTPUT="$(run_bare --check --cli --next 2>&1)" RC=$? set -e [[ "$RC" -ne 0 ]] grep -qF -- "Required command not found: node" <<<"$OUTPUT" [[ ! -d "$NODE_HOME" ]] reset_home echo "[test] a tampered download is rejected and nothing is installed" publish_release "$VERSION" true set +e OUTPUT="$(run_bare --cli --next --yes --no-auto-launch 2>&1)" RC=$? set -e [[ "$RC" -ne 0 ]] grep -qF -- "failed checksum verification" <<<"$OUTPUT" [[ ! -x "${NODE_HOME}/${VERSION}/bin/node" ]] publish_release "$VERSION" reset_home echo "[test] a system Node that is new enough is used as-is and left alone" write_node_stub "$FAKE_BIN/node" 22 write_npm_stub "$FAKE_BIN/npm" OUTPUT="$( env -u npm_config_prefix \ HOME="$HOME_DIR" \ MOSAIC_HOME="$MOSAIC_HOME_DIR" \ MOSAIC_PREFIX="$PREFIX" \ MOSAIC_NO_COLOR=1 \ MOSAIC_NODE_HOME="$NODE_HOME" \ MOSAIC_NODE_DIST="file://${DIST}" \ MOSAIC_TEST_REAL_NODE="$REAL_NODE" \ MOSAIC_TEST_NPM_LOG="$LOG" \ MOSAIC_TEST_STATE="$STATE" \ PATH="$FAKE_BIN:$NONODE_BIN" \ bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1 )" grep -qF -- "satisfies the >= 22 requirement" <<<"$OUTPUT" [[ ! -d "$NODE_HOME" ]] reset_home echo "[test] a system Node that is too old is replaced rather than accepted" write_node_stub "$FAKE_BIN/node" 18 OUTPUT="$( env -u npm_config_prefix \ HOME="$HOME_DIR" \ MOSAIC_HOME="$MOSAIC_HOME_DIR" \ MOSAIC_PREFIX="$PREFIX" \ MOSAIC_NO_COLOR=1 \ MOSAIC_NODE_HOME="$NODE_HOME" \ MOSAIC_NODE_DIST="file://${DIST}" \ MOSAIC_TEST_REAL_NODE="$REAL_NODE" \ MOSAIC_TEST_NPM_LOG="$LOG" \ MOSAIC_TEST_STATE="$STATE" \ PATH="$FAKE_BIN:$NONODE_BIN" \ bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1 )" grep -qF -- "older than the required >= 22" <<<"$OUTPUT" [[ -x "${NODE_HOME}/${VERSION}/bin/node" ]] echo "[test] installer node provisioning tests passed"