publish.sh's overwrite guard (fdroid-repo-ci#6) is correct but implies a contract nothing states: a colliding filename with no versionCode in it means only the first publish into a prefix can ever succeed. Flutter's constant app-<flavor>-release.apk hit this in weblib-viewer (#29), and the failure only surfaces on the second publish with an error that reads like a bucket-state problem. Add the naming contract to the README, and append a hint to the refusal message when the colliding object's basename has no digits at all -- a pure text addition, never changing which publishes are refused. Closes fdroid-repo-ci#13. Co-authored-by: bit <bit@das-labor.org>
501 lines
26 KiB
Bash
Executable File
501 lines
26 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Tests for publish.sh -- the one script in this repo that needs real
|
|
# network to do its actual job (fdroid-repo-ci#1), so its happy path against
|
|
# a genuine bucket is out of scope for most of this file. What this asserts:
|
|
#
|
|
# * argument/credential validation, offline;
|
|
# * `--dry-run` builds the exact command(s) a real run would use for
|
|
# whichever of --files-only/--index-only (or neither -> both, files then
|
|
# index) is selected, offline;
|
|
# * a real invocation against a closed local port fails on the network call
|
|
# itself -- during the credential check specifically, before any upload is
|
|
# attempted -- proving both that the constructed command is well-formed
|
|
# and that verification genuinely comes first (fdroid-repo-ci#5);
|
|
# * a real invocation against a tiny local HEAD-only S3 stub (test_s3_stub.py)
|
|
# proves the credential check is a genuine authenticated call (a wrong
|
|
# bucket is rejected, not waved through because a variable was merely
|
|
# non-empty), that an already-published APK is refused before any upload
|
|
# (and that --index-only alone never runs that check at all, since it
|
|
# cannot upload an APK) -- and, as a negative control, that a
|
|
# *non*-colliding APK does NOT trip the same check (fdroid-repo-ci#6);
|
|
# * the opt-in live round trip against a real bucket, extended to prove the
|
|
# two-pass ordering **by construction**: `--files-only` then `--index-only`
|
|
# as two genuinely separate invocations, with the bucket state asserted in
|
|
# between -- no synchronization hook, no `eval` of anything in this public
|
|
# repo's production script -- plus the APK-collision refusal against a
|
|
# real object, and a deliberate-misuse case proving `--index-only` alone
|
|
# really does produce the exact state the ordering guarantee exists to
|
|
# prevent (an index naming a file that is not there), which the *default*
|
|
# path never reaches.
|
|
set -euo pipefail
|
|
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
|
# Captured BEFORE the fake credentials below are exported. Those fakes are
|
|
# deliberate -- they keep the offline tests from ever reaching a real service
|
|
# -- but they also overwrite the real ones for the rest of this file, so the
|
|
# live round trip at the bottom has to hold on to the originals here or it
|
|
# would authenticate as AKIAFAKETESTKEY and fail every time. A live test that
|
|
# always fails gets disabled, which is worse than not having one.
|
|
LIVE_KEY_ID="${AWS_ACCESS_KEY_ID:-}"
|
|
LIVE_SECRET="${AWS_SECRET_ACCESS_KEY:-}"
|
|
LIVE_REGION="${AWS_DEFAULT_REGION:-}"
|
|
source "$here/test_lib.sh"
|
|
|
|
PYTHON="python3"
|
|
have_python=false
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
have_python=true
|
|
elif py=$(bash "$here/with-nixpkgs.sh" python3 sh -c 'command -v python3' 2>/dev/null); then
|
|
have_python=true
|
|
fi
|
|
|
|
work=$(mktemp -d)
|
|
trap 'rm -rf "$work"' EXIT
|
|
mkdir -p "$work/repo"
|
|
echo '{}' > "$work/repo/index-v2.json"
|
|
|
|
echo "--- missing credentials are refused, not sent as empty ---"
|
|
assert_failure "publish.sh refuses with no AWS_ACCESS_KEY_ID" \
|
|
env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY \
|
|
bash "$here/publish.sh" "$work/repo" mybucket https://s3.example.invalid
|
|
|
|
export AWS_ACCESS_KEY_ID=AKIAFAKETESTKEY
|
|
export AWS_SECRET_ACCESS_KEY=fake-secret-not-real
|
|
|
|
echo "--- missing local directory is refused ---"
|
|
assert_failure "publish.sh refuses a non-existent local directory" \
|
|
bash "$here/publish.sh" "$work/does-not-exist" mybucket https://s3.example.invalid
|
|
|
|
echo "--- --dry-run builds two commands, files then index, touches no network ---"
|
|
out=$(bash "$here/publish.sh" --dry-run "$work/repo" mybucket https://s3.example.invalid myprefix)
|
|
assert_contains "dry-run names the destination bucket and prefix" "$out" "s3://mybucket/myprefix"
|
|
assert_contains "dry-run names the endpoint" "$out" "--endpoint-url https://s3.example.invalid"
|
|
assert_contains "dry-run goes through with-nixpkgs.sh, same as every other script" "$out" "with-nixpkgs.sh"
|
|
if echo "$out" | grep -q -- --delete; then
|
|
fail "dry-run does not silently add --delete (it did!)"
|
|
else
|
|
pass "dry-run does not silently add --delete"
|
|
fi
|
|
|
|
nlines=$(echo "$out" | grep -c 's3 sync' || true)
|
|
if [ "$nlines" -eq 2 ]; then
|
|
pass "dry-run prints exactly two sync commands (files pass, index pass)"
|
|
else
|
|
fail "dry-run prints exactly two sync commands (files pass, index pass) (got $nlines)"
|
|
fi
|
|
|
|
files_line=$(echo "$out" | grep -n 's3 sync' | sed -n '1p' | cut -d: -f1)
|
|
index_line=$(echo "$out" | grep -n 's3 sync' | sed -n '2p' | cut -d: -f1)
|
|
files_cmd=$(echo "$out" | sed -n "${files_line}p")
|
|
index_cmd=$(echo "$out" | sed -n "${index_line}p")
|
|
|
|
assert_contains "the first (files) pass excludes index-shaped names" "$files_cmd" "--exclude index"
|
|
assert_contains "the first (files) pass excludes entry-shaped names" "$files_cmd" "--exclude entry."
|
|
if echo "$files_cmd" | grep -q -- "--include"; then
|
|
fail "the first (files) pass has no --include (it did!)"
|
|
else
|
|
pass "the first (files) pass has no --include"
|
|
fi
|
|
assert_contains "the second (index) pass includes index-shaped names" "$index_cmd" "--include index"
|
|
assert_contains "the second (index) pass includes entry-shaped names" "$index_cmd" "--include entry."
|
|
assert_contains "the second (index) pass excludes everything else first" "$index_cmd" "--exclude"
|
|
if [ "$files_line" -lt "$index_line" ]; then
|
|
pass "the files pass is printed before the index pass -- index really does go last"
|
|
else
|
|
fail "the files pass is printed before the index pass (files at line $files_line, index at $index_line)"
|
|
fi
|
|
|
|
echo "--- --dry-run with no prefix omits the trailing slash entirely ---"
|
|
out_noprefix=$(bash "$here/publish.sh" --dry-run "$work/repo" mybucket https://s3.example.invalid)
|
|
assert_contains "no-prefix dry-run targets the bucket root" "$out_noprefix" "s3://mybucket "
|
|
|
|
echo "--- --files-only and --index-only each print exactly one command, and are mutually exclusive ---"
|
|
out_files_only=$(bash "$here/publish.sh" --dry-run --files-only "$work/repo" mybucket https://s3.example.invalid myprefix)
|
|
n_files_only=$(echo "$out_files_only" | grep -c 's3 sync' || true)
|
|
if [ "$n_files_only" -eq 1 ]; then
|
|
pass "--files-only prints exactly one sync command"
|
|
else
|
|
fail "--files-only prints exactly one sync command (got $n_files_only)"
|
|
fi
|
|
assert_contains "the --files-only command still excludes index-shaped names" "$out_files_only" "--exclude index"
|
|
if echo "$out_files_only" | grep -q -- "--include"; then
|
|
fail "--files-only has no --include (it did!)"
|
|
else
|
|
pass "--files-only has no --include"
|
|
fi
|
|
|
|
out_index_only=$(bash "$here/publish.sh" --dry-run --index-only "$work/repo" mybucket https://s3.example.invalid myprefix)
|
|
n_index_only=$(echo "$out_index_only" | grep -c 's3 sync' || true)
|
|
if [ "$n_index_only" -eq 1 ]; then
|
|
pass "--index-only prints exactly one sync command"
|
|
else
|
|
fail "--index-only prints exactly one sync command (got $n_index_only)"
|
|
fi
|
|
assert_contains "the --index-only command still includes index-shaped names" "$out_index_only" "--include index"
|
|
|
|
assert_failure "publish.sh refuses --files-only together with --index-only" \
|
|
bash "$here/publish.sh" --dry-run --files-only --index-only "$work/repo" mybucket https://s3.example.invalid
|
|
|
|
echo "--- a real invocation fails on the network call, not on arguments -- and fails during credential verification, before any upload ---"
|
|
# port 1 is a reserved, unassigned TCP port -- nothing listens there, so this
|
|
# never leaves the loopback interface and needs no real network access.
|
|
real_out=$(bash "$here/publish.sh" "$work/repo" mybucket "http://127.0.0.1:1" 2>&1) && rc=0 || rc=$?
|
|
if [ "${rc:-0}" -ne 0 ]; then pass "publish.sh against a closed port exits non-zero"; else
|
|
fail "publish.sh against a closed port exits non-zero (got 0)"
|
|
fi
|
|
assert_contains "the failure is a connection error, not an argument error" "$real_out" "Could not connect"
|
|
assert_contains "publish.sh announces credential verification" "$real_out" "verifying credentials"
|
|
if echo "$real_out" | grep -q "pass 1/2"; then
|
|
fail "publish.sh must not start pass 1 when credential verification never completed (it did!)"
|
|
else
|
|
pass "publish.sh never starts pass 1 when credential verification fails"
|
|
fi
|
|
|
|
# --- offline, against a local HEAD-only S3 stub -----------------------------
|
|
# Real S3 semantics (200/404 over HTTP, path-style addressing since the
|
|
# endpoint host here is a literal IP) without a real bucket or real
|
|
# credentials -- see test_s3_stub.py's own docstring for exactly what it
|
|
# does and does not implement, and why that is enough for these two checks
|
|
# without ever letting publish.sh reach `aws s3 sync`.
|
|
if [ "$have_python" = true ]; then
|
|
start_stub() {
|
|
# start_stub <bucket> [<existing-key> ...] -- prints the chosen port on
|
|
# stdout, leaves the server running in the background; caller kills $stub_pid.
|
|
local logf
|
|
logf=$(mktemp)
|
|
bash "$here/with-nixpkgs.sh" python3 python3 "$here/test_s3_stub.py" "$@" \
|
|
>"$logf" 2>&1 &
|
|
stub_pid=$!
|
|
for _ in $(seq 1 50); do
|
|
[ -s "$logf" ] && break
|
|
sleep 0.1
|
|
done
|
|
stub_port=$(head -1 "$logf")
|
|
stub_log=$logf
|
|
}
|
|
|
|
echo "--- credential verification is a real authenticated call, not just 'is it set' ---"
|
|
start_stub realbucket
|
|
out=$(bash "$here/publish.sh" "$work/repo" wrongbucket "http://127.0.0.1:$stub_port" 2>&1) && rc=0 || rc=$?
|
|
kill "$stub_pid" 2>/dev/null || true
|
|
wait "$stub_pid" 2>/dev/null || true
|
|
if [ "${rc:-0}" -ne 0 ]; then
|
|
pass "publish.sh refuses a bucket the stub does not recognise (bad/unusable credentials)"
|
|
else
|
|
fail "publish.sh refuses a bucket the stub does not recognise (bad/unusable credentials) (got exit 0)"
|
|
fi
|
|
assert_contains "the rejection is reported before any upload" "$out" "credentials rejected"
|
|
if echo "$out" | grep -q "pass 1/2"; then
|
|
fail "publish.sh must not start pass 1 after a rejected credential check (it did!)"
|
|
else
|
|
pass "publish.sh never starts pass 1 after a rejected credential check"
|
|
fi
|
|
|
|
echo "--- an already-published APK is refused, before any upload (fdroid-repo-ci#6) ---"
|
|
collide_dir="$work/collide"
|
|
mkdir -p "$collide_dir"
|
|
echo '{}' > "$collide_dir/index-v2.json"
|
|
echo 'unsigned bytes racing the real one' > "$collide_dir/de.example.app_1.apk"
|
|
start_stub mybucket "myprefix/de.example.app_1.apk"
|
|
out=$(bash "$here/publish.sh" "$collide_dir" mybucket "http://127.0.0.1:$stub_port" myprefix 2>&1) && rc=0 || rc=$?
|
|
kill "$stub_pid" 2>/dev/null || true
|
|
wait "$stub_pid" 2>/dev/null || true
|
|
if [ "${rc:-0}" -ne 0 ]; then
|
|
pass "publish.sh refuses to publish over an already-existing APK"
|
|
else
|
|
fail "publish.sh refuses to publish over an already-existing APK (got exit 0)"
|
|
fi
|
|
assert_contains "the refusal names the colliding key" "$out" "myprefix/de.example.app_1.apk"
|
|
assert_contains "the refusal explains why" "$out" "already exists"
|
|
if echo "$out" | grep -q "pass 1/2"; then
|
|
fail "publish.sh must not start pass 1 after refusing a colliding APK (it did!)"
|
|
else
|
|
pass "publish.sh never starts pass 1 after refusing a colliding APK"
|
|
fi
|
|
# de.example.app_1.apk carries a digit (the versionCode "1"), so the
|
|
# no-digits hint below (fdroid-repo-ci#13) must not fire for it -- this is
|
|
# the negative control for that hint, proving it is not just always on.
|
|
if echo "$out" | grep -q "does not appear to carry a versionCode"; then
|
|
fail "publish.sh must not hint about a missing versionCode when the colliding filename has digits (it did!)"
|
|
else
|
|
pass "publish.sh does not add the no-versionCode hint when the colliding filename has digits"
|
|
fi
|
|
|
|
echo "--- a colliding filename with no digits gets the versionCode hint (fdroid-repo-ci#13) ---"
|
|
# The Flutter case the issue is about: a build that always emits the same
|
|
# filename regardless of version, e.g. app-dev-release.apk. The guard
|
|
# still refuses exactly as above; only the message grows a line pointing
|
|
# at the naming contract, since a bucket-state-shaped error is what took a
|
|
# full CI cycle to diagnose in weblib-viewer#29.
|
|
nodigits_dir="$work/nodigits"
|
|
mkdir -p "$nodigits_dir"
|
|
echo '{}' > "$nodigits_dir/index-v2.json"
|
|
echo 'unsigned bytes, same name every build' > "$nodigits_dir/app-dev-release.apk"
|
|
start_stub mybucket "myprefix/app-dev-release.apk"
|
|
out=$(bash "$here/publish.sh" "$nodigits_dir" mybucket "http://127.0.0.1:$stub_port" myprefix 2>&1) && rc=0 || rc=$?
|
|
kill "$stub_pid" 2>/dev/null || true
|
|
wait "$stub_pid" 2>/dev/null || true
|
|
if [ "${rc:-0}" -ne 0 ]; then
|
|
pass "publish.sh refuses to publish a colliding APK filename with no digits"
|
|
else
|
|
fail "publish.sh refuses to publish a colliding APK filename with no digits (got exit 0)"
|
|
fi
|
|
assert_contains "the refusal names the colliding key" "$out" "myprefix/app-dev-release.apk"
|
|
assert_contains "the refusal explains why" "$out" "already exists"
|
|
assert_contains "the refusal hints that the filename carries no versionCode" "$out" \
|
|
"does not appear to carry a versionCode"
|
|
assert_contains "the hint states F-Droid's naming convention" "$out" \
|
|
"<applicationId>_<versionCode>.apk"
|
|
if echo "$out" | grep -q "pass 1/2"; then
|
|
fail "publish.sh must not start pass 1 after refusing a colliding no-digits APK (it did!)"
|
|
else
|
|
pass "publish.sh never starts pass 1 after refusing a colliding no-digits APK"
|
|
fi
|
|
|
|
echo "--- negative control: a non-colliding APK does not trip the same check ---"
|
|
start_stub mybucket # nothing pre-exists
|
|
out=$(bash "$here/publish.sh" "$collide_dir" mybucket "http://127.0.0.1:$stub_port" myprefix 2>&1) && rc=0 || rc=$?
|
|
kill "$stub_pid" 2>/dev/null || true
|
|
wait "$stub_pid" 2>/dev/null || true
|
|
# The stub has no sync/PUT support at all, so pass 1 is expected to fail
|
|
# once it gets there -- the only thing this proves is that the collision
|
|
# check itself did not false-positive and let the script proceed past it.
|
|
assert_contains "with nothing pre-existing, publish.sh proceeds to pass 1" "$out" "pass 1/2"
|
|
|
|
echo "--- --index-only alone never runs the APK-collision check at all (it cannot upload one) ---"
|
|
# Same colliding key pre-seeded as the refusal test above, but this time
|
|
# asking only for the index pass: the check must not even run, since
|
|
# index_only_filters can never select a *.apk, so there is nothing it
|
|
# could refuse. Proceeding to "pass 2/2" (and failing there, since this
|
|
# stub has no sync support) is exactly the proof: a version of publish.sh
|
|
# that ran the check regardless of the flags would instead refuse here,
|
|
# before ever printing "pass 2/2".
|
|
start_stub mybucket "myprefix/de.example.app_1.apk"
|
|
out=$(bash "$here/publish.sh" --index-only "$collide_dir" mybucket "http://127.0.0.1:$stub_port" myprefix 2>&1) && rc=0 || rc=$?
|
|
kill "$stub_pid" 2>/dev/null || true
|
|
wait "$stub_pid" 2>/dev/null || true
|
|
assert_contains "--index-only proceeds straight to pass 2/2, skipping the APK check" "$out" "pass 2/2"
|
|
if echo "$out" | grep -q "already exists"; then
|
|
fail "--index-only must not run the APK-collision check (it refused anyway!)"
|
|
else
|
|
pass "--index-only does not run the APK-collision check"
|
|
fi
|
|
else
|
|
skip "credential-check-is-real / APK-collision-refusal (no python3 available, even via with-nixpkgs.sh)"
|
|
skip "APK-collision refusal includes the no-versionCode hint for a digit-free filename"
|
|
skip "negative control: non-colliding APK does not trip the check"
|
|
skip "--index-only never runs the APK-collision check"
|
|
fi
|
|
|
|
# --- live round trip, opt-in ------------------------------------------------
|
|
# Skipped unless a scratch bucket and credentials are present, so the default
|
|
# offline run is unchanged. This is the only part of the suite that talks to
|
|
# a real server, and the only part that can prove the two-pass *ordering*
|
|
# with real uploads rather than command construction, and the APK-collision
|
|
# refusal against a real, already-published object.
|
|
if [ -n "${FDROID_CI_LIVE_BUCKET:-}" ] && [ -n "${FDROID_CI_LIVE_ENDPOINT:-}" ] \
|
|
&& [ -n "$LIVE_KEY_ID" ] && [ -n "$LIVE_SECRET" ]; then
|
|
echo "--- a real round trip against a real endpoint ---"
|
|
export AWS_ACCESS_KEY_ID="$LIVE_KEY_ID"
|
|
export AWS_SECRET_ACCESS_KEY="$LIVE_SECRET"
|
|
[ -n "$LIVE_REGION" ] && export AWS_DEFAULT_REGION="$LIVE_REGION"
|
|
# publish.sh sets these itself, but only inside its own (child) process --
|
|
# it does not propagate back here, and every direct `aws s3 cp -` *write*
|
|
# this file does below needs it too, or it fails exactly like the Garage
|
|
# checksum bug documented in publish.sh (InvalidRequest: invalid checksum
|
|
# algorithm), which reads like a permissions problem.
|
|
export AWS_REQUEST_CHECKSUM_CALCULATION="${AWS_REQUEST_CHECKSUM_CALCULATION:-when_required}"
|
|
export AWS_RESPONSE_CHECKSUM_VALIDATION="${AWS_RESPONSE_CHECKSUM_VALIDATION:-when_required}"
|
|
live_dir=$(mktemp -d)
|
|
stamp="live-$(date -u +%s)-$$"
|
|
printf 'publish.sh live probe %s\n' "$stamp" > "$live_dir/probe.txt"
|
|
printf '{"probe":"%s"}\n' "$stamp" > "$live_dir/index-v2.json"
|
|
|
|
cleanup_live() {
|
|
bash "$here/with-nixpkgs.sh" awscli2 aws --endpoint-url "$FDROID_CI_LIVE_ENDPOINT" \
|
|
s3 rm --recursive "s3://$FDROID_CI_LIVE_BUCKET/$stamp/" >/dev/null 2>&1 || true
|
|
rm -rf "$live_dir"
|
|
}
|
|
|
|
object_exists() {
|
|
# object_exists <key> -- a real head-object against the live bucket.
|
|
bash "$here/with-nixpkgs.sh" awscli2 aws --endpoint-url "$FDROID_CI_LIVE_ENDPOINT" \
|
|
--region "${LIVE_REGION:-us-east-1}" \
|
|
s3api head-object --bucket "$FDROID_CI_LIVE_BUCKET" --key "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
echo "--- upload ordering by construction: --files-only, assert, then --index-only, assert (the hard part -- see below) ---"
|
|
# This is the one property the offline suite structurally cannot see: it
|
|
# is about the order two *real* uploads land in, which only a real bucket
|
|
# can answer. Racing a check in from outside against one real invocation
|
|
# doing both passes is exactly the flakiness fdroid-repo-ci#5 warned off
|
|
# ("worth building the check into the script's own output rather than
|
|
# trying to catch a race from outside") -- and, per review on #7, doing
|
|
# that with an env-var-triggered `eval` synchronization hook inside
|
|
# publish.sh was its own problem in a public script that runs with a
|
|
# signing key and S3 credentials in its environment. --files-only and
|
|
# --index-only remove the need for either: they are two genuinely separate
|
|
# invocations, so the bucket can be inspected for real, in between, with
|
|
# nothing racing and nothing evaluated.
|
|
bash "$here/publish.sh" --files-only "$live_dir" "$FDROID_CI_LIVE_BUCKET" \
|
|
"$FDROID_CI_LIVE_ENDPOINT" "$stamp" >/tmp/publish_live_out.$$ 2>&1
|
|
files_only_rc=$?
|
|
if [ "$files_only_rc" -eq 0 ]; then
|
|
pass "--files-only uploads to a real bucket"
|
|
else
|
|
fail "--files-only uploads to a real bucket (exit $files_only_rc)"
|
|
cat /tmp/publish_live_out.$$ >&2
|
|
fi
|
|
rm -f /tmp/publish_live_out.$$
|
|
|
|
if object_exists "$stamp/probe.txt"; then
|
|
pass "after --files-only alone: the file object exists"
|
|
else
|
|
fail "after --files-only alone: the file object exists"
|
|
fi
|
|
if object_exists "$stamp/index-v2.json"; then
|
|
fail "after --files-only alone: the index object must not exist yet (it does!)"
|
|
else
|
|
pass "after --files-only alone: the index object does not exist yet"
|
|
fi
|
|
|
|
bash "$here/publish.sh" --index-only "$live_dir" "$FDROID_CI_LIVE_BUCKET" \
|
|
"$FDROID_CI_LIVE_ENDPOINT" "$stamp" >/tmp/publish_live_out2.$$ 2>&1
|
|
index_only_rc=$?
|
|
if [ "$index_only_rc" -eq 0 ]; then
|
|
pass "--index-only uploads to a real bucket"
|
|
else
|
|
fail "--index-only uploads to a real bucket (exit $index_only_rc)"
|
|
cat /tmp/publish_live_out2.$$ >&2
|
|
fi
|
|
rm -f /tmp/publish_live_out2.$$
|
|
|
|
if object_exists "$stamp/index-v2.json"; then
|
|
pass "after --index-only resumes: the index object now exists too"
|
|
else
|
|
fail "after --index-only resumes: the index object now exists too"
|
|
fi
|
|
|
|
got=$(bash "$here/with-nixpkgs.sh" awscli2 aws --endpoint-url "$FDROID_CI_LIVE_ENDPOINT" \
|
|
s3 cp "s3://$FDROID_CI_LIVE_BUCKET/$stamp/probe.txt" - 2>/dev/null || true)
|
|
if [ "$got" = "publish.sh live probe $stamp" ]; then
|
|
pass "the uploaded file object reads back byte-identical"
|
|
else
|
|
fail "the uploaded file object reads back byte-identical"
|
|
fi
|
|
got_index=$(bash "$here/with-nixpkgs.sh" awscli2 aws --endpoint-url "$FDROID_CI_LIVE_ENDPOINT" \
|
|
s3 cp "s3://$FDROID_CI_LIVE_BUCKET/$stamp/index-v2.json" - 2>/dev/null || true)
|
|
if [ "$got_index" = "{\"probe\":\"$stamp\"}" ]; then
|
|
pass "the uploaded index object (pass 2) reads back byte-identical"
|
|
else
|
|
fail "the uploaded index object (pass 2) reads back byte-identical"
|
|
fi
|
|
|
|
echo "--- fault injection: --index-only alone (misuse) really does produce the dangerous state; the default never does ---"
|
|
# The property that actually matters: --index-only in isolation, against a
|
|
# prefix that has never had --files-only run against it, must produce
|
|
# exactly the state the whole ordering guarantee exists to prevent -- an
|
|
# index naming a file that 404s. If it did not, the two flags would not be
|
|
# a faithful decomposition of the default and the "by construction" claim
|
|
# above would be empty.
|
|
danger_dir=$(mktemp -d)
|
|
danger_stamp="live-danger-$(date -u +%s)-$$"
|
|
printf 'publish.sh live probe %s\n' "$danger_stamp" > "$danger_dir/probe.txt"
|
|
printf '{"probe":"%s"}\n' "$danger_stamp" > "$danger_dir/index-v2.json"
|
|
|
|
bash "$here/publish.sh" --index-only "$danger_dir" "$FDROID_CI_LIVE_BUCKET" \
|
|
"$FDROID_CI_LIVE_ENDPOINT" "$danger_stamp" >/tmp/publish_danger.$$ 2>&1
|
|
danger_rc=$?
|
|
if [ "$danger_rc" -ne 0 ]; then
|
|
fail "misuse setup: --index-only alone uploads to a real bucket (exit $danger_rc)"
|
|
cat /tmp/publish_danger.$$ >&2
|
|
fi
|
|
rm -f /tmp/publish_danger.$$
|
|
|
|
if object_exists "$danger_stamp/index-v2.json"; then
|
|
pass "misuse (--index-only alone) really does publish an index"
|
|
else
|
|
fail "misuse (--index-only alone) really does publish an index"
|
|
fi
|
|
if object_exists "$danger_stamp/probe.txt"; then
|
|
fail "misuse (--index-only alone) must leave the file it names absent (it does not!)"
|
|
else
|
|
pass "misuse (--index-only alone) leaves an index naming a file that 404s -- exactly the state the ordering guarantee exists to prevent"
|
|
fi
|
|
|
|
bash "$here/with-nixpkgs.sh" awscli2 aws --endpoint-url "$FDROID_CI_LIVE_ENDPOINT" \
|
|
s3 rm --recursive "s3://$FDROID_CI_LIVE_BUCKET/$danger_stamp/" >/dev/null 2>&1 || true
|
|
rm -rf "$danger_dir"
|
|
|
|
# Contrast: the default (neither flag, one invocation) must never reach
|
|
# that state. There is deliberately no mid-flight observation point for a
|
|
# single default invocation any more (that was the hook this PR removes),
|
|
# so this is not re-proven by racing a check against it -- it holds by
|
|
# construction instead: the default's first phase runs the *same command
|
|
# array*, built the same way, as --files-only above, so its effect on the
|
|
# bucket is identical -- files land, and only then does the index command
|
|
# run. There is no code path through publish.sh that reaches "index
|
|
# present, file absent" without --index-only having been invoked alone,
|
|
# which is opt-in and never the default. What is left to check here is
|
|
# only the default's own final state.
|
|
safe_dir=$(mktemp -d)
|
|
safe_stamp="live-safe-$(date -u +%s)-$$"
|
|
printf 'publish.sh live probe %s\n' "$safe_stamp" > "$safe_dir/probe.txt"
|
|
printf '{"probe":"%s"}\n' "$safe_stamp" > "$safe_dir/index-v2.json"
|
|
bash "$here/publish.sh" "$safe_dir" "$FDROID_CI_LIVE_BUCKET" \
|
|
"$FDROID_CI_LIVE_ENDPOINT" "$safe_stamp" >/tmp/publish_safe.$$ 2>&1
|
|
safe_rc=$?
|
|
rm -f /tmp/publish_safe.$$
|
|
if [ "$safe_rc" -eq 0 ] && object_exists "$safe_stamp/probe.txt" && object_exists "$safe_stamp/index-v2.json"; then
|
|
pass "the default path (neither flag) never leaves an index without its file: both are present after one call"
|
|
else
|
|
fail "the default path (neither flag) never leaves an index without its file"
|
|
fi
|
|
bash "$here/with-nixpkgs.sh" awscli2 aws --endpoint-url "$FDROID_CI_LIVE_ENDPOINT" \
|
|
s3 rm --recursive "s3://$FDROID_CI_LIVE_BUCKET/$safe_stamp/" >/dev/null 2>&1 || true
|
|
rm -rf "$safe_dir"
|
|
|
|
echo "--- APK-collision refusal against a real, already-published object ---"
|
|
apk_stamp="live-apk-$(date -u +%s)-$$"
|
|
apk_key="$apk_stamp/de.example.app_1.apk"
|
|
original_bytes="original apk bytes $apk_stamp"
|
|
printf '%s' "$original_bytes" | bash "$here/with-nixpkgs.sh" awscli2 aws \
|
|
--endpoint-url "$FDROID_CI_LIVE_ENDPOINT" --region "${LIVE_REGION:-us-east-1}" \
|
|
s3 cp - "s3://$FDROID_CI_LIVE_BUCKET/$apk_key" >/dev/null 2>&1
|
|
|
|
race_dir=$(mktemp -d)
|
|
echo '{}' > "$race_dir/index-v2.json"
|
|
printf 'a different build racing the same versionCode' > "$race_dir/de.example.app_1.apk"
|
|
|
|
race_out=$(bash "$here/publish.sh" "$race_dir" "$FDROID_CI_LIVE_BUCKET" \
|
|
"$FDROID_CI_LIVE_ENDPOINT" "$apk_stamp" 2>&1) && race_rc=0 || race_rc=$?
|
|
if [ "${race_rc:-0}" -ne 0 ]; then
|
|
pass "publish.sh refuses a second publish that would overwrite a real, already-published APK"
|
|
else
|
|
fail "publish.sh refuses a second publish that would overwrite a real, already-published APK (got exit 0)"
|
|
fi
|
|
assert_contains "the real-bucket refusal names the colliding key" "$race_out" "$apk_key"
|
|
|
|
after_bytes=$(bash "$here/with-nixpkgs.sh" awscli2 aws --endpoint-url "$FDROID_CI_LIVE_ENDPOINT" \
|
|
s3 cp "s3://$FDROID_CI_LIVE_BUCKET/$apk_key" - 2>/dev/null || true)
|
|
if [ "$after_bytes" = "$original_bytes" ]; then
|
|
pass "the original APK's bytes are unchanged after the refused publish"
|
|
else
|
|
fail "the original APK's bytes are unchanged after the refused publish"
|
|
fi
|
|
|
|
bash "$here/with-nixpkgs.sh" awscli2 aws --endpoint-url "$FDROID_CI_LIVE_ENDPOINT" \
|
|
s3 rm --recursive "s3://$FDROID_CI_LIVE_BUCKET/$apk_stamp/" >/dev/null 2>&1 || true
|
|
rm -rf "$race_dir"
|
|
|
|
cleanup_live
|
|
else
|
|
skip "live round trip (set FDROID_CI_LIVE_BUCKET, FDROID_CI_LIVE_ENDPOINT and AWS credentials)"
|
|
skip "live upload ordering by construction (--files-only then --index-only)"
|
|
skip "live fault injection: --index-only misuse vs. the default"
|
|
skip "live APK-collision refusal against a real object"
|
|
fi
|
|
|
|
report_and_exit
|