#!/usr/bin/env bash # Regression harness for pr-merge.sh identity-first principal resolution # (mosaicstack/stack #1280). # # Covers: # 1. --dry-run reports the acting principal the merge WOULD use, resolved the # same way the real merge resolves it: --login > MOSAIC_GIT_IDENTITY / # git config mosaic.gitIdentity > shared host credential. (The pre-#1280 # deployed copy reported a tea login that the merge would not act as.) # 2. --dry-run fails closed when the requested principal has no credential: # unknown --login, or an identity with no per-slot token (stderr names # the login / the identity and its slot path). # 3. The real merge POST carries the resolved principal's credential and no # other: --login merges with that login's tea-config token; an identity # merges with the per-slot token; an unresolvable --login never reaches # the provider. # # Fixture pattern from test-pr-merge-head-pin.sh: the scripts under test are # copied into a fixture tree with stubbed pr-metadata.sh / ci-queue-wait.sh # siblings; the provider is a stubbed curl that records the credential it # received. NEVER reads real secrets or hits a live forge. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-principal-resolution}" FAKE_HOME="$WORK_DIR/home" REPO_DIR="$WORK_DIR/repo" TOOLS_DIR="$WORK_DIR/tools" BIN_DIR="$WORK_DIR/bin" LOG_FILE="$WORK_DIR/calls.log" CREDENTIALS_FILE="$FAKE_HOME/.config/mosaic/credentials.json" SHA=0123456789abcdef0123456789abcdef01234567 rm -rf "$WORK_DIR" mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" "$FAKE_HOME/.config/tea" \ "$REPO_DIR" "$TOOLS_DIR/git" "$TOOLS_DIR/_lib" "$BIN_DIR" cp "$SCRIPT_DIR/pr-merge.sh" "$TOOLS_DIR/git/pr-merge.sh" cp "$SCRIPT_DIR/detect-platform.sh" "$TOOLS_DIR/git/detect-platform.sh" cp "$SCRIPT_DIR/../_lib/credentials.sh" "$TOOLS_DIR/_lib/credentials.sh" chmod +x "$TOOLS_DIR/git/pr-merge.sh" git -C "$REPO_DIR" init -q git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git # Stubbed siblings pr-merge.sh resolves relative to its own SCRIPT_DIR. cat > "$TOOLS_DIR/git/pr-metadata.sh" < "$TOOLS_DIR/git/ci-queue-wait.sh" <<'SH' #!/usr/bin/env bash exit 0 SH chmod +x "$TOOLS_DIR/git/pr-metadata.sh" "$TOOLS_DIR/git/ci-queue-wait.sh" cat > "$CREDENTIALS_FILE" <<'JSON' { "gitea": { "mosaicstack": { "url": "https://git.mosaicstack.dev", "token": "shared-mosaicstack-token" } } } JSON cat > "$FAKE_HOME/.config/tea/config.yml" <<'YAML' logins: - name: fred-ms url: https://git.mosaicstack.dev token: SECRET-fred-ms-tea-token YAML echo -n "SECRET-agentX-slot-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentX.token" : > "$LOG_FILE" # Stubbed tea for login-list resolution only. cat > "$BIN_DIR/tea" <<'SH' #!/usr/bin/env bash set -euo pipefail if [[ "$*" == "login list --output json" ]]; then cat <<'JSON' [ {"name":"fred-ms","url":"https://git.mosaicstack.dev","default":true} ] JSON exit 0 fi exit 0 SH chmod +x "$BIN_DIR/tea" # Stubbed provider. pr-merge passes curl config on STDIN with -K -; the stub # reads stdin, records the Authorization header it received, answers 200. cat > "$BIN_DIR/curl" <> "$LOG_FILE" [[ -n "\$out_file" ]] && printf '{}' > "\$out_file" printf '200\n' exit 0 SH chmod +x "$BIN_DIR/curl" fail=0 assert_contains_log() { local desc="$1" needle="$2" if ! grep -qF -- "$needle" "$LOG_FILE"; then echo "FAIL: $desc — log does not contain '$needle':" >&2 cat "$LOG_FILE" >&2 fail=1 fi } assert_not_contains_log() { local desc="$1" needle="$2" if grep -qF -- "$needle" "$LOG_FILE"; then echo "FAIL: $desc — log must not contain '$needle':" >&2 cat "$LOG_FILE" >&2 fail=1 fi } run_pr_merge() { local extra_args="$1"; shift ( cd "$REPO_DIR" # shellcheck disable=SC2086 # extra_args is deliberately word-split wrapper args env -i HOME="$FAKE_HOME" PATH="$BIN_DIR:$PATH" \ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \ MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" "$@" \ bash "$TOOLS_DIR/git/pr-merge.sh" -n 42 $extra_args ) } # --------------------------------------------------------------------------- # 1. --dry-run reports the resolved acting principal truthfully. # --------------------------------------------------------------------------- out=$(run_pr_merge "--dry-run" MOSAIC_GIT_IDENTITY=agentX) if [[ "$out" != *"as git identity 'agentX' (per-slot credential)"* ]]; then echo "FAIL: dry-run identity — principal not reported: $out" >&2 fail=1 fi out=$(run_pr_merge "--dry-run --login fred-ms" MOSAIC_GIT_IDENTITY=agentX) if [[ "$out" != *"as tea login 'fred-ms'"* ]]; then echo "FAIL: dry-run login override — login not reported (must beat env identity): $out" >&2 fail=1 fi out=$(run_pr_merge "--dry-run") if [[ "$out" != *"as default host credential"* ]]; then echo "FAIL: dry-run default — not reported: $out" >&2 fail=1 fi # --------------------------------------------------------------------------- # 2. --dry-run fails closed when the requested principal has no credential. # --------------------------------------------------------------------------- stderr_file="$WORK_DIR/stderr.tmp" set +e out=$(run_pr_merge "--dry-run --login ghost" 2>"$stderr_file") rc=$? set -e if [[ "$rc" -eq 0 ]] || [[ "$(cat "$stderr_file")" != *"ghost"* ]]; then echo "FAIL: dry-run unknown --login — expected fail-loud naming 'ghost', rc=$rc" >&2 cat "$stderr_file" >&2 fail=1 fi : > "$stderr_file" set +e out=$(run_pr_merge "--dry-run" MOSAIC_GIT_IDENTITY=agentNoSlot 2>"$stderr_file") rc=$? set -e err=$(cat "$stderr_file") if [[ "$rc" -eq 0 ]] || [[ "$err" != *"agentNoSlot"* ]] \ || [[ "$err" != *"$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentNoSlot.token"* ]]; then echo "FAIL: dry-run identity without slot — expected fail-loud naming identity + slot path, rc=$rc" >&2 echo "$err" >&2 fail=1 fi # --------------------------------------------------------------------------- # 3. The real merge POST carries the resolved principal's credential ONLY. # --------------------------------------------------------------------------- : > "$LOG_FILE" set +e out=$(run_pr_merge "--login fred-ms" MOSAIC_GIT_IDENTITY=agentX 2>"$stderr_file") rc=$? set -e if [[ "$rc" -ne 0 ]]; then echo "FAIL: merge with --login — expected rc=0, got $rc" >&2 cat "$stderr_file" >&2 fail=1 fi assert_contains_log "merge --login uses the login token" "CURL-AUTH: Authorization: token SECRET-fred-ms-tea-token" assert_not_contains_log "merge --login must not use the identity slot token" "SECRET-agentX-slot-token" assert_not_contains_log "merge --login must not use the shared token" "shared-mosaicstack-token" : > "$LOG_FILE" set +e out=$(run_pr_merge "" MOSAIC_GIT_IDENTITY=agentX 2>"$stderr_file") rc=$? set -e if [[ "$rc" -ne 0 ]]; then echo "FAIL: merge with identity — expected rc=0, got $rc" >&2 cat "$stderr_file" >&2 fail=1 fi assert_contains_log "merge identity uses the per-slot token" "CURL-AUTH: Authorization: token SECRET-agentX-slot-token" assert_not_contains_log "merge identity must not use the shared token" "shared-mosaicstack-token" : > "$LOG_FILE" set +e out=$(run_pr_merge "--login ghost" 2>"$stderr_file") rc=$? set -e if [[ "$rc" -eq 0 ]]; then echo "FAIL: merge with unknown --login — expected nonzero, got 0" >&2 fail=1 fi assert_not_contains_log "merge with unknown --login must not reach the provider" "CURL-URL" if [[ "$fail" -eq 0 ]]; then echo "pr-merge identity-first principal resolution regression passed" fi exit "$fail"