542 lines
24 KiB
Bash
Executable File
542 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Regression harness for the optional, identity-checked Gitea squash message.
|
|
|
|
set -u
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SUBJECT="${MOSAIC_TEST_SUBJECT:-$SCRIPT_DIR/pr-merge.sh}"
|
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-message-field}"
|
|
ORIG_PATH="$PATH"
|
|
failures=0
|
|
|
|
rm -rf "$WORK_DIR"
|
|
mkdir -p "$WORK_DIR"
|
|
|
|
fail() {
|
|
echo "FAIL $1" >&2
|
|
failures=$((failures + 1))
|
|
}
|
|
|
|
make_case() {
|
|
local name="$1" case_dir
|
|
case_dir="$WORK_DIR/$name"
|
|
mkdir -p "$case_dir/bin" "$case_dir/agent"
|
|
cp "$SUBJECT" "$case_dir/pr-merge.sh"
|
|
chmod +x "$case_dir/pr-merge.sh"
|
|
|
|
cat > "$case_dir/detect-platform.sh" <<'SH'
|
|
#!/usr/bin/env bash
|
|
detect_platform() { PLATFORM=gitea; printf 'gitea\n'; }
|
|
get_repo_owner() { printf 'acme\n'; }
|
|
get_repo_name() { printf 'widgets\n'; }
|
|
get_remote_host() { printf 'git.example.test\n'; }
|
|
get_gitea_token() {
|
|
printf 'resolved\n' >> "${MOSAIC_TEST_TOKEN_RESOLUTION_LOG:?}"
|
|
if [[ "${MOSAIC_TEST_TOKEN_AVAILABLE:-true}" != "true" ]]; then
|
|
return 1
|
|
fi
|
|
printf 'fixture-token\n'
|
|
}
|
|
get_gitea_basic_auth() {
|
|
printf 'resolved\n' >> "${MOSAIC_TEST_BASIC_RESOLUTION_LOG:?}"
|
|
if [[ "${MOSAIC_TEST_BASIC_AVAILABLE:-false}" == "true" ]]; then
|
|
printf 'fixture-user:fixture-password\n'
|
|
return "${MOSAIC_TEST_BASIC_RC:-0}"
|
|
fi
|
|
return 1
|
|
}
|
|
get_gitea_login_for_host() { return 1; }
|
|
SH
|
|
|
|
cat > "$case_dir/pr-metadata.sh" <<'SH'
|
|
#!/usr/bin/env bash
|
|
if [[ "${MOSAIC_TEST_TITLE_MODE:-safe}" == "injection" ]]; then
|
|
title='Preserve authors\n\nCo-authored-by: victim <[email protected]>'
|
|
else
|
|
title='Preserve both branch authors'
|
|
fi
|
|
case "${MOSAIC_TEST_COMMITS_MODE:?}" in
|
|
verified) head_sha=2222222222222222222222222222222222222222 ;;
|
|
null-login|unsafe-identity) head_sha=3333333333333333333333333333333333333333 ;;
|
|
single) head_sha=1111111111111111111111111111111111111111 ;;
|
|
*) echo "unknown commits mode" >&2; exit 2 ;;
|
|
esac
|
|
printf '{"number":42,"title":"%s","author":"poster","baseRefName":"main","headRefName":"feature/fixture","headRefOid":"%s","headRepository":"acme/widgets"}\n' "$title" "$head_sha"
|
|
SH
|
|
|
|
cat > "$case_dir/ci-queue-wait.sh" <<'SH'
|
|
#!/usr/bin/env bash
|
|
exit 0
|
|
SH
|
|
|
|
cat > "$case_dir/bin/python3" <<'SH'
|
|
#!/usr/bin/env bash
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
*"Preserve both branch authors"*|*"[email protected]"*)
|
|
: > "${MOSAIC_TEST_METADATA_ARGV_MARKER:?}"
|
|
;;
|
|
esac
|
|
done
|
|
exec "${MOSAIC_TEST_REAL_PYTHON:?}" "$@"
|
|
SH
|
|
|
|
cat > "$case_dir/bin/curl" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -eu
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
*"Preserve both branch authors"*|*"[email protected]"*)
|
|
: > "${MOSAIC_TEST_METADATA_ARGV_MARKER:?}"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
url=""
|
|
method="GET"
|
|
out_file=""
|
|
data=""
|
|
config=""
|
|
auth_mode="none"
|
|
has_max_filesize=0
|
|
has_max_time=0
|
|
has_connect_timeout=0
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-o)
|
|
out_file="$2"
|
|
shift 2
|
|
;;
|
|
-w)
|
|
shift 2
|
|
;;
|
|
-X)
|
|
method="$2"
|
|
shift 2
|
|
;;
|
|
-d|--data|--data-binary)
|
|
data="$2"
|
|
if [[ "$data" == @* ]]; then
|
|
data=$(<"${data#@}")
|
|
fi
|
|
shift 2
|
|
;;
|
|
-K|--config)
|
|
if [[ "$2" == "-" ]]; then
|
|
config=$(cat)
|
|
fi
|
|
shift 2
|
|
;;
|
|
--max-filesize)
|
|
has_max_filesize=1
|
|
shift 2
|
|
;;
|
|
--max-time)
|
|
has_max_time=1
|
|
shift 2
|
|
;;
|
|
--connect-timeout)
|
|
has_connect_timeout=1
|
|
shift 2
|
|
;;
|
|
-H|--header|-u|--user)
|
|
if [[ "$2" == *"fixture-token"* ]]; then
|
|
: > "${MOSAIC_TEST_TOKEN_ARGV_MARKER:?}"
|
|
fi
|
|
if [[ "$2" == *"fixture-password"* ]]; then
|
|
: > "${MOSAIC_TEST_BASIC_ARGV_MARKER:?}"
|
|
fi
|
|
shift 2
|
|
;;
|
|
http://*|https://*)
|
|
url="$1"
|
|
shift
|
|
;;
|
|
*)
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$config" == *"Authorization: token fixture-token"* ]]; then
|
|
auth_mode="token"
|
|
: > "${MOSAIC_TEST_AUTH_CONFIG_MARKER:?}"
|
|
elif [[ "$config" == *"user = \"fixture-user:fixture-password\""* ]]; then
|
|
auth_mode="basic"
|
|
: > "${MOSAIC_TEST_BASIC_CONFIG_MARKER:?}"
|
|
fi
|
|
printf '%s %s %s\n' "$method" "$auth_mode" "$url" >> "${MOSAIC_TEST_CURL_LOG:?}"
|
|
printf '%s:%s:%s\n' "$has_max_filesize" "$has_max_time" "$has_connect_timeout" >> "${MOSAIC_TEST_CURL_BOUNDS_LOG:?}"
|
|
|
|
case "$url" in
|
|
*/pulls/42)
|
|
case "${MOSAIC_TEST_COMMITS_MODE:?}" in
|
|
verified) head_sha=2222222222222222222222222222222222222222 ;;
|
|
null-login|unsafe-identity) head_sha=3333333333333333333333333333333333333333 ;;
|
|
single) head_sha=1111111111111111111111111111111111111111 ;;
|
|
*) echo "unknown commits mode" >&2; exit 2 ;;
|
|
esac
|
|
if [[ "${MOSAIC_TEST_HEAD_MODE:-stable}" == "moved" ]]; then
|
|
head_sha=4444444444444444444444444444444444444444
|
|
fi
|
|
body="{\"head\":{\"sha\":\"$head_sha\"}}"
|
|
code=200
|
|
if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "inspection" && "$auth_mode" == "token" ]]; then
|
|
body='{"message":"token rejected"}'
|
|
code=401
|
|
fi
|
|
;;
|
|
*/pulls/42/commits*)
|
|
case "${MOSAIC_TEST_COMMITS_MODE:?}" in
|
|
verified)
|
|
if [[ "${MOSAIC_TEST_EMAIL_MODE:-safe}" == "escape" ]]; then
|
|
body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"alice+\u001b[[email protected]"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
|
|
else
|
|
body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"[email protected]"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
|
|
fi
|
|
;;
|
|
null-login)
|
|
body='[{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}},{"sha":"3333333333333333333333333333333333333333","commit":{"author":{"name":"Unresolved Author","email":"[email protected]\n\u001b[31m"}},"author":null}]'
|
|
;;
|
|
unsafe-identity)
|
|
body='[{"sha":"unsafe\n\u001b[31m","commit":{"author":{"name":"Unsafe","email":"not-an-email"}},"author":{"login":"unsafe"}},{"sha":"3333333333333333333333333333333333333333","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
|
|
;;
|
|
single)
|
|
body='[{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
|
|
;;
|
|
*)
|
|
echo "unknown commits mode" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
code=200
|
|
if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "inspection" && "$auth_mode" == "token" ]]; then
|
|
body='{"message":"token rejected"}'
|
|
code=401
|
|
fi
|
|
;;
|
|
*/pulls/42/merge)
|
|
body='{}'
|
|
code=200
|
|
if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "merge" && "$auth_mode" == "token" ]]; then
|
|
body='{"message":"token rejected"}'
|
|
code=401
|
|
elif [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "provider-error" ]]; then
|
|
body='{"message":"branch policy rejected\n\u001b[31m"}'
|
|
code=409
|
|
elif [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "forbidden" ]]; then
|
|
body='{"message":"permission denied"}'
|
|
code=403
|
|
else
|
|
printf '%s' "$data" > "${MOSAIC_TEST_MERGE_PAYLOAD:?}"
|
|
fi
|
|
;;
|
|
*/users/*)
|
|
body='{"message":"not found"}'
|
|
code=404
|
|
;;
|
|
*)
|
|
body='{"message":"unexpected URL"}'
|
|
code=500
|
|
;;
|
|
esac
|
|
|
|
if [[ -n "$out_file" ]]; then
|
|
printf '%s' "$body" > "$out_file"
|
|
else
|
|
printf '%s' "$body"
|
|
fi
|
|
printf '%s' "$code"
|
|
case "${MOSAIC_TEST_CURL_FAILURE:-none}" in
|
|
oversize) exit 63 ;;
|
|
stalled) exit 28 ;;
|
|
esac
|
|
SH
|
|
|
|
chmod +x "$case_dir/detect-platform.sh" "$case_dir/pr-metadata.sh" \
|
|
"$case_dir/ci-queue-wait.sh" "$case_dir/bin/curl" "$case_dir/bin/python3"
|
|
printf '%s\n' "$case_dir"
|
|
}
|
|
|
|
run_case() {
|
|
local case_dir="$1" mode="$2"
|
|
shift 2
|
|
MOSAIC_TEST_COMMITS_MODE="$mode" \
|
|
MOSAIC_TEST_CURL_LOG="$case_dir/curl.log" \
|
|
MOSAIC_TEST_CURL_BOUNDS_LOG="$case_dir/curl-bounds.log" \
|
|
MOSAIC_TEST_MERGE_PAYLOAD="$case_dir/merge-payload.json" \
|
|
MOSAIC_TEST_TOKEN_ARGV_MARKER="$case_dir/token-in-argv" \
|
|
MOSAIC_TEST_BASIC_ARGV_MARKER="$case_dir/basic-in-argv" \
|
|
MOSAIC_TEST_AUTH_CONFIG_MARKER="$case_dir/auth-via-config" \
|
|
MOSAIC_TEST_BASIC_CONFIG_MARKER="$case_dir/basic-via-config" \
|
|
MOSAIC_TEST_TOKEN_RESOLUTION_LOG="$case_dir/token-resolution.log" \
|
|
MOSAIC_TEST_BASIC_RESOLUTION_LOG="$case_dir/basic-resolution.log" \
|
|
MOSAIC_TEST_METADATA_ARGV_MARKER="$case_dir/metadata-in-argv" \
|
|
MOSAIC_TEST_REAL_PYTHON="$(command -v python3)" \
|
|
AGENT_WORK_ROOT="$case_dir/agent" \
|
|
PATH="$case_dir/bin:$ORIG_PATH" \
|
|
"$case_dir/pr-merge.sh" -n 42 "$@"
|
|
}
|
|
|
|
# Verified multi-author path: the non-poster trailer is built from one commit's
|
|
# linked author.login and that same commit's author email. No /users lookup.
|
|
verified_dir=$(make_case verified)
|
|
set +e
|
|
verified_output=$(run_case "$verified_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
verified_rc=$?
|
|
set -e
|
|
if [[ "$verified_rc" -ne 0 ]]; then
|
|
fail "verified multi-author merge expected rc=0, got rc=$verified_rc: $verified_output"
|
|
elif [[ ! -s "$verified_dir/merge-payload.json" ]]; then
|
|
fail "verified multi-author merge did not reach the API payload"
|
|
else
|
|
python3 - "$verified_dir/merge-payload.json" <<'PY' || fail "verified payload did not preserve squash and exact message fields"
|
|
import json
|
|
import sys
|
|
payload = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
assert payload == {
|
|
"Do": "squash",
|
|
"head_commit_id": "2222222222222222222222222222222222222222",
|
|
"MergeTitleField": "Preserve both branch authors",
|
|
"MergeMessageField": "Co-authored-by: alice <[email protected]>",
|
|
}, payload
|
|
PY
|
|
fi
|
|
[[ -e "$verified_dir/auth-via-config" ]] || fail "verified path did not authenticate curl through stdin config"
|
|
[[ ! -e "$verified_dir/token-in-argv" ]] || fail "verified path placed the Gitea token in curl argv"
|
|
[[ ! -e "$verified_dir/metadata-in-argv" ]] || fail "verified path placed PR title or contributor email in child argv"
|
|
[[ "$(wc -l < "$verified_dir/token-resolution.log")" -eq 1 ]] || fail "verified path did not bind inspection and merge to one credential resolution"
|
|
if grep -q '/users/' "$verified_dir/curl.log" 2>/dev/null; then
|
|
fail "verified path performed a forbidden second /users lookup"
|
|
fi
|
|
if grep -qv '^1:1:1$' "$verified_dir/curl-bounds.log"; then
|
|
fail "verified path did not apply size/max-time/connect-time bounds to every provider download"
|
|
fi
|
|
|
|
# A linked email containing a terminal escape must block before mutation.
|
|
escape_email_dir=$(make_case escape-email)
|
|
set +e
|
|
escape_email_output=$(MOSAIC_TEST_EMAIL_MODE=escape run_case "$escape_email_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
escape_email_rc=$?
|
|
set -e
|
|
[[ "$escape_email_rc" -ne 0 ]] || fail "control-byte email unexpectedly passed"
|
|
[[ "$escape_email_output" == *"unusable linked identity"* ]] || fail "control-byte email refusal lost its diagnostic"
|
|
[[ ! -e "$escape_email_dir/merge-payload.json" ]] || fail "control-byte email reached the merge API"
|
|
|
|
# Curl transfer and duration failures must remain failures even with HTTP 200.
|
|
for failure_mode in oversize stalled; do
|
|
failure_dir=$(make_case "curl-$failure_mode")
|
|
set +e
|
|
failure_output=$(MOSAIC_TEST_CURL_FAILURE="$failure_mode" run_case "$failure_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
failure_rc=$?
|
|
set -e
|
|
[[ "$failure_rc" -ne 0 ]] || fail "curl $failure_mode failure was discarded: $failure_output"
|
|
[[ ! -e "$failure_dir/merge-payload.json" ]] || fail "curl $failure_mode failure reached the merge API"
|
|
done
|
|
|
|
# The authenticated head is re-read under the mutation credential but cannot
|
|
# replace the canonical preflight/review head. A move blocks before enumeration
|
|
# or mutation even though the provider returned a valid new SHA.
|
|
moved_dir=$(make_case moved-head)
|
|
set +e
|
|
moved_output=$(MOSAIC_TEST_HEAD_MODE=moved \
|
|
run_case "$moved_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
moved_rc=$?
|
|
set -e
|
|
[[ "$moved_rc" -ne 0 ]] || fail "moved authenticated head unexpectedly passed"
|
|
[[ "$moved_output" == *"authenticated PR head moved from reviewed"* ]] || fail "moved head refusal lost its diagnostic"
|
|
[[ "$moved_output" == *"tl-mosaic"* ]] || fail "moved head refusal omitted the named escalation principal"
|
|
[[ ! -e "$moved_dir/merge-payload.json" ]] || fail "moved head refusal reached the merge API"
|
|
moved_sequence=$(awk '{print $1 ":" $2}' "$moved_dir/curl.log" | paste -sd, -)
|
|
[[ "$moved_sequence" == "GET:token" ]] || fail "moved head refusal performed post-move inspection/mutation (calls=$moved_sequence)"
|
|
|
|
# Token resolution failure is not an authentication response. It must fail
|
|
# closed instead of borrowing a Basic credential under a different principal.
|
|
token_missing_dir=$(make_case token-missing)
|
|
set +e
|
|
token_missing_output=$(MOSAIC_TEST_TOKEN_AVAILABLE=false MOSAIC_TEST_BASIC_AVAILABLE=true \
|
|
run_case "$token_missing_dir" single 2>&1)
|
|
token_missing_rc=$?
|
|
set -e
|
|
[[ "$token_missing_rc" -ne 0 ]] || fail "missing token unexpectedly borrowed Basic Auth"
|
|
[[ "$token_missing_output" == *"required Gitea token"* ]] || fail "missing token refusal lost its diagnostic"
|
|
[[ ! -e "$token_missing_dir/basic-resolution.log" ]] || fail "missing token resolved Basic Auth after identity failure"
|
|
[[ ! -e "$token_missing_dir/curl.log" ]] || fail "missing token reached a provider request"
|
|
|
|
# A failed Basic resolver must never use its nonempty output or reach mutation.
|
|
basic_rc_dir=$(make_case basic-resolver-rc)
|
|
set +e
|
|
basic_rc_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_BASIC_RC=91 MOSAIC_TEST_FALLBACK_MODE=inspection \
|
|
run_case "$basic_rc_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
basic_rc_rc=$?
|
|
set -e
|
|
[[ "$basic_rc_rc" -ne 0 ]] || fail "failed Basic resolver output unexpectedly authorized a merge: $basic_rc_output"
|
|
[[ ! -e "$basic_rc_dir/merge-payload.json" ]] || fail "failed Basic resolver reached the merge API"
|
|
|
|
# HTTP 401 never changes principals: inspection rejection fails closed without
|
|
# resolving or attempting Basic Auth.
|
|
fallback_inspect_dir=$(make_case fallback-inspection)
|
|
set +e
|
|
fallback_inspect_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=inspection \
|
|
run_case "$fallback_inspect_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
fallback_inspect_rc=$?
|
|
set -e
|
|
[[ "$fallback_inspect_rc" -ne 0 ]] || fail "inspection token rejection unexpectedly changed principals"
|
|
[[ "$fallback_inspect_output" == *"refusing cross-principal credential fallback"* ]] || fail "inspection token rejection lost its refusal diagnostic"
|
|
[[ ! -e "$fallback_inspect_dir/basic-resolution.log" ]] || fail "inspection token rejection resolved Basic Auth"
|
|
[[ ! -e "$fallback_inspect_dir/merge-payload.json" ]] || fail "inspection token rejection reached merge mutation"
|
|
inspect_sequence=$(awk '{print $1 ":" $2}' "$fallback_inspect_dir/curl.log" | paste -sd, -)
|
|
[[ "$inspect_sequence" == "GET:token" ]] || fail "inspection rejection made unexpected provider calls (calls=$inspect_sequence)"
|
|
|
|
# Token rejection at merge likewise fails closed without cross-principal retry.
|
|
fallback_merge_dir=$(make_case fallback-merge)
|
|
set +e
|
|
fallback_merge_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=merge \
|
|
run_case "$fallback_merge_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
fallback_merge_rc=$?
|
|
set -e
|
|
[[ "$fallback_merge_rc" -ne 0 ]] || fail "merge token rejection unexpectedly changed principals"
|
|
[[ "$fallback_merge_output" == *"refusing cross-principal credential fallback"* ]] || fail "merge token rejection lost its refusal diagnostic"
|
|
[[ ! -e "$fallback_merge_dir/basic-resolution.log" ]] || fail "merge token rejection resolved Basic Auth"
|
|
[[ ! -e "$fallback_merge_dir/merge-payload.json" ]] || fail "merge token rejection recorded a successful payload"
|
|
merge_sequence=$(awk '{print $1 ":" $2}' "$fallback_merge_dir/curl.log" | paste -sd, -)
|
|
[[ "$merge_sequence" == "GET:token,GET:token,POST:token" ]] || fail "merge rejection made unexpected provider calls (calls=$merge_sequence)"
|
|
|
|
# BLOCK path: a commit email exists but author.login is null. It must name both
|
|
# facts, name the escalation principal, and never reach the merge endpoint.
|
|
null_dir=$(make_case null-login)
|
|
set +e
|
|
null_output=$(run_case "$null_dir" null-login --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
null_rc=$?
|
|
set -e
|
|
[[ "$null_rc" -ne 0 ]] || fail "null-login author expected a non-zero BLOCK"
|
|
[[ "$null_output" == *"BLOCK"* ]] || fail "null-login author omitted BLOCK diagnostic"
|
|
[[ "$null_output" == *"author.login=NULL"* ]] || fail "null-login author omitted the null provider fact"
|
|
[[ "$null_output" == *"[email protected]"* ]] || fail "null-login author omitted the commit email fact"
|
|
[[ "$null_output" == *'\n\x1b[31m'* ]] || fail "null-login author diagnostic did not escape control characters"
|
|
[[ "$null_output" != *$'\033'* ]] || fail "null-login author diagnostic emitted a raw terminal escape"
|
|
[[ "$(printf '%s\n' "$null_output" | wc -l)" -eq 1 ]] || fail "null-login author diagnostic permitted newline injection"
|
|
[[ "$null_output" == *"tl-mosaic"* ]] || fail "null-login author omitted the named escalation principal"
|
|
[[ ! -e "$null_dir/merge-payload.json" ]] || fail "null-login BLOCK still reached the merge API"
|
|
|
|
# Every provider-derived field in alternate BLOCK diagnostics is log-safe too,
|
|
# including an invalid non-head SHA that contains control characters.
|
|
unsafe_dir=$(make_case unsafe-identity)
|
|
set +e
|
|
unsafe_output=$(run_case "$unsafe_dir" unsafe-identity --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
unsafe_rc=$?
|
|
set -e
|
|
[[ "$unsafe_rc" -ne 0 ]] || fail "unsafe identity expected a non-zero BLOCK"
|
|
[[ "$unsafe_output" == *"unusable linked identity"* ]] || fail "unsafe identity omitted its BLOCK reason"
|
|
[[ "$unsafe_output" == *'\n\x1b[31m'* ]] || fail "unsafe identity SHA did not escape control characters"
|
|
[[ "$unsafe_output" != *$'\033'* ]] || fail "unsafe identity diagnostic emitted a raw terminal escape"
|
|
[[ "$(printf '%s\n' "$unsafe_output" | wc -l)" -eq 1 ]] || fail "unsafe identity diagnostic permitted newline injection"
|
|
[[ ! -e "$unsafe_dir/merge-payload.json" ]] || fail "unsafe identity BLOCK still reached the merge API"
|
|
|
|
# The provider PR title cannot add an unchecked trailer outside the constructed
|
|
# message field: multi-line and trailer-shaped titles block before mutation.
|
|
title_dir=$(make_case title-injection)
|
|
set +e
|
|
title_output=$(MOSAIC_TEST_TITLE_MODE=injection \
|
|
run_case "$title_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
|
|
title_rc=$?
|
|
set -e
|
|
[[ "$title_rc" -ne 0 ]] || fail "title trailer injection unexpectedly passed"
|
|
[[ "$title_output" == *"not one printable, non-trailer line"* ]] || fail "title injection refusal lost its diagnostic"
|
|
[[ ! -e "$title_dir/merge-payload.json" ]] || fail "title injection reached the merge API"
|
|
|
|
# Provider failures remain diagnosable after their temporary response file is
|
|
# removed, but provider-controlled control characters stay log-safe.
|
|
error_dir=$(make_case provider-error)
|
|
set +e
|
|
error_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=provider-error \
|
|
run_case "$error_dir" single 2>&1)
|
|
error_rc=$?
|
|
set -e
|
|
[[ "$error_rc" -ne 0 ]] || fail "provider error unexpectedly passed"
|
|
[[ "$error_output" == *"HTTP 409"* ]] || fail "provider error omitted the HTTP status"
|
|
[[ "$error_output" == *"branch policy rejected"* ]] || fail "provider error response was discarded"
|
|
[[ "$error_output" == *'\n\x1b[31m'* ]] || fail "provider error response did not escape control characters"
|
|
[[ "$error_output" != *$'\033'* ]] || fail "provider error response emitted a raw terminal escape"
|
|
[[ "$error_output" != *"Basic Auth fallback"* ]] || fail "provider error advertised removed Basic Auth fallback"
|
|
[[ ! -e "$error_dir/basic-resolution.log" ]] || fail "HTTP 409 policy denial incorrectly triggered Basic Auth fallback"
|
|
|
|
# Authorization denials likewise fail closed instead of changing principals.
|
|
forbidden_dir=$(make_case forbidden)
|
|
set +e
|
|
forbidden_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=forbidden \
|
|
run_case "$forbidden_dir" single 2>&1)
|
|
forbidden_rc=$?
|
|
set -e
|
|
[[ "$forbidden_rc" -ne 0 ]] || fail "HTTP 403 authorization denial unexpectedly passed"
|
|
[[ "$forbidden_output" == *"HTTP 403"* ]] || fail "authorization denial omitted the HTTP status"
|
|
[[ "$forbidden_output" != *"Basic Auth fallback"* ]] || fail "authorization denial advertised removed Basic Auth fallback"
|
|
[[ ! -e "$forbidden_dir/basic-resolution.log" ]] || fail "HTTP 403 authorization denial incorrectly triggered Basic Auth fallback"
|
|
|
|
# The BLOCK destination cannot be generic or inferred after failure: opting in
|
|
# without a named principal is refused before any provider operation.
|
|
principal_dir=$(make_case missing-principal)
|
|
set +e
|
|
principal_output=$(run_case "$principal_dir" verified --co-author-trailers 2>&1)
|
|
principal_rc=$?
|
|
set -e
|
|
[[ "$principal_rc" -ne 0 ]] || fail "co-author mode without a named principal unexpectedly passed"
|
|
[[ "$principal_output" == *"requires --escalate-to with a named principal"* ]] || fail "missing-principal refusal lost its diagnostic"
|
|
[[ ! -e "$principal_dir/merge-payload.json" ]] || fail "missing-principal refusal reached the merge API"
|
|
|
|
# A trailing value-taking option receives a stable CLI diagnostic instead of a
|
|
# set -u unbound-variable crash.
|
|
value_dir=$(make_case missing-principal-value)
|
|
set +e
|
|
value_output=$(run_case "$value_dir" verified --co-author-trailers --escalate-to 2>&1)
|
|
value_rc=$?
|
|
set -e
|
|
[[ "$value_rc" -ne 0 ]] || fail "missing --escalate-to value unexpectedly passed"
|
|
[[ "$value_output" == *"--escalate-to requires one principal name"* ]] || fail "missing --escalate-to value lost its diagnostic"
|
|
[[ "$value_output" != *"unbound variable"* ]] || fail "missing --escalate-to value crashed under set -u"
|
|
[[ ! -e "$value_dir/merge-payload.json" ]] || fail "missing --escalate-to value reached the merge API"
|
|
|
|
# Negative control: ordinary single-author merge remains byte-for-byte payload
|
|
# compatible and hardcoded to squash, with no optional message fields.
|
|
single_dir=$(make_case single)
|
|
set +e
|
|
single_output=$(run_case "$single_dir" single 2>&1)
|
|
single_rc=$?
|
|
set -e
|
|
if [[ "$single_rc" -ne 0 ]]; then
|
|
fail "ordinary single-author merge expected rc=0, got rc=$single_rc: $single_output"
|
|
elif [[ ! -s "$single_dir/merge-payload.json" ]]; then
|
|
fail "ordinary single-author merge did not reach the API payload"
|
|
else
|
|
python3 - "$single_dir/merge-payload.json" <<'PY' || fail "ordinary single-author payload changed"
|
|
import json
|
|
import sys
|
|
payload = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
assert payload == {
|
|
"Do": "squash",
|
|
"head_commit_id": "1111111111111111111111111111111111111111",
|
|
}, payload
|
|
PY
|
|
fi
|
|
[[ -e "$single_dir/auth-via-config" ]] || fail "ordinary path did not authenticate curl through stdin config"
|
|
[[ ! -e "$single_dir/token-in-argv" ]] || fail "ordinary path placed the Gitea token in curl argv"
|
|
[[ "$(wc -l < "$single_dir/token-resolution.log")" -eq 1 ]] || fail "ordinary path did not use exactly one credential resolution"
|
|
|
|
# Squash is not defaultable: an explicit non-squash method must remain refused.
|
|
method_dir=$(make_case method-refusal)
|
|
set +e
|
|
method_output=$(run_case "$method_dir" single -m merge 2>&1)
|
|
method_rc=$?
|
|
set -e
|
|
[[ "$method_rc" -ne 0 ]] || fail "non-squash method unexpectedly passed"
|
|
[[ "$method_output" == *"enforces squash merge only"* ]] || fail "non-squash refusal lost its policy diagnostic"
|
|
[[ ! -e "$method_dir/merge-payload.json" ]] || fail "non-squash refusal reached the merge API"
|
|
|
|
if [[ "$failures" -ne 0 ]]; then
|
|
echo "pr-merge message-field regression failed ($failures assertions)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "pr-merge message-field regression passed (verified, BLOCK, and unchanged squash control)"
|