test: verify Android public staging shell

This commit is contained in:
SimpleTest 2026-07-19 20:13:00 +03:00
parent 44865635e4
commit c6aa3d1ed2
2 changed files with 286 additions and 0 deletions

View File

@ -60,6 +60,19 @@ be published as a release. The manifest accepts same-origin HTTPS deep links,
but verified Android App Links additionally require the final signing but verified Android App Links additionally require the final signing
certificate fingerprint in the deployment's `/.well-known/assetlinks.json`. certificate fingerprint in the deployment's `/.well-known/assetlinks.json`.
With the temporary public origin reachable, run the API 37 emulator smoke test:
```sh
./scripts/android-staging-smoke.sh
```
The script installs the exported staging APK into a fresh project-scoped
emulator container, loads the configured HTTPS home page, follows a
same-origin `/safety` deep link, verifies that the package does not claim an
external HTTPS origin, and retains UI dumps, screenshots, package metadata,
and logcat diagnostics under ignored `output/android-staging-smoke/`. The
one-run container and image are removed on success or failure.
## Reproducible Docker build ## Reproducible Docker build
From the repository root: From the repository root:

273
scripts/android-staging-smoke.sh Executable file
View File

@ -0,0 +1,273 @@
#!/bin/sh
set -eu
umask 077
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
ENV_FILE="$ROOT/.env"
APK="$ROOT/android/dist-staging/who-need-help-staging.apk"
run_id=$(date -u +%Y%m%d%H%M%S)-$$
image="who-need-help-android:staging-smoke-$run_id"
container="who-need-help-android-staging-smoke-$run_id"
output="$ROOT/output/android-staging-smoke/$run_id"
package=org.whoneedhelp.mobile.staging
activity=org.whoneedhelp.mobile.MainActivity
if [ ! -e /dev/kvm ]; then
echo "/dev/kvm is required for the containerized Android emulator." >&2
exit 1
fi
if [ ! -f "$ENV_FILE" ]; then
echo "Missing $ENV_FILE." >&2
exit 1
fi
if [ ! -s "$APK" ]; then
echo "Missing staging APK: $APK. Run scripts/android-staging-build.sh first." >&2
exit 1
fi
set -a
# shellcheck source=/dev/null
. "$ENV_FILE"
set +a
: "${WNH_BASE_URL:?Set WNH_BASE_URL in .env}"
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
case "$WNH_BASE_URL" in
https://*/* | https://*) ;;
*)
echo "WNH_BASE_URL must be an HTTPS origin." >&2
exit 1
;;
esac
origin_without_scheme=${WNH_BASE_URL#https://}
case "$origin_without_scheme" in
"" | */* | *\?* | *\#* | *@*)
echo "WNH_BASE_URL must be a credential-free HTTPS origin without a path." >&2
exit 1
;;
esac
expected_host=${origin_without_scheme%%:*}
mkdir -p "$output"
chmod 700 "$ROOT/output" "$ROOT/output/android-staging-smoke" "$output"
cleanup() {
status=$?
trap - EXIT HUP INT TERM
if docker inspect "$container" >/dev/null 2>&1; then
docker exec "$container" adb logcat -d \
>"$output/logcat.txt" 2>&1 || true
docker exec "$container" adb shell dumpsys activity activities \
>"$output/activities.txt" 2>&1 || true
docker logs "$container" >"$output/emulator.log" 2>&1 || true
fi
docker rm -f "$container" >/dev/null 2>&1 || true
docker image rm "$image" >/dev/null 2>&1 || true
{
printf 'container_absent='
if docker inspect "$container" >/dev/null 2>&1; then
printf 'false\n'
else
printf 'true\n'
fi
printf 'image_absent='
if docker image inspect "$image" >/dev/null 2>&1; then
printf 'false\n'
else
printf 'true\n'
fi
} >"$output/cleanup.txt"
exit "$status"
}
trap cleanup EXIT HUP INT TERM
sha256sum "$APK" >"$output/apk.sha256"
docker build \
--build-arg "WNH_DEBUG_BASE_URL=$WNH_BASE_URL" \
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
--build-arg "WNH_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \
--build-arg \
"ANDROID_EMULATOR_SYSTEM_IMAGE=system-images/android-37.0/google_apis_ps16k/x86_64" \
--target emulator \
--tag "$image" \
"$ROOT/android" >"$output/image-build.txt"
docker run -d \
--name "$container" \
--device /dev/kvm \
"$image" >"$output/container-id.txt"
docker exec "$container" adb wait-for-device
booted=
attempt=0
while [ "$attempt" -lt 90 ]; do
booted=$(
docker exec "$container" adb shell getprop sys.boot_completed 2>/dev/null |
tr -d '\r'
)
if [ "$booted" = "1" ]; then
break
fi
attempt=$((attempt + 1))
sleep 2
done
if [ "$booted" != "1" ]; then
echo "Android staging emulator did not finish booting." >&2
exit 1
fi
docker exec "$container" adb shell input keyevent 82
docker exec "$container" adb shell settings put global window_animation_scale 0
docker exec "$container" adb shell settings put global transition_animation_scale 0
docker exec "$container" adb shell settings put global animator_duration_scale 0
docker cp "$APK" "$container:/tmp/who-need-help-staging.apk"
docker exec "$container" adb install -r /tmp/who-need-help-staging.apk \
>"$output/install.txt"
docker exec "$container" adb shell dumpsys package "$package" \
>"$output/package.txt"
if ! grep -Fq "versionName=0.1.0-staging" "$output/package.txt"; then
echo "The installed package is not the expected staging variant." >&2
exit 1
fi
same_origin="$WNH_BASE_URL/safety"
external_origin=https://example.com/
if ! grep -Fq "Authority: \"$expected_host\"" "$output/package.txt"; then
echo "The staging APK does not declare its exact HTTPS host." >&2
exit 1
fi
if grep -Fq 'Authority: "example.com"' "$output/package.txt"; then
echo "The staging APK incorrectly declares an external HTTPS host." >&2
exit 1
fi
docker exec "$container" adb shell cmd package resolve-activity --brief \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "$same_origin" >"$output/same-origin-resolver.txt"
docker exec "$container" adb shell cmd package resolve-activity --brief \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "$external_origin" >"$output/external-origin-resolver.txt"
if grep -Fq "$package/" "$output/external-origin-resolver.txt"; then
echo "The staging APK incorrectly claimed an external HTTPS origin." >&2
exit 1
fi
docker exec "$container" adb shell pm get-app-links "$package" \
>"$output/app-links.txt"
docker exec "$container" adb logcat -c
docker exec "$container" adb shell am start -W \
-n "$package/$activity" >"$output/home-start.txt"
home_loaded=false
attempt=0
while [ "$attempt" -lt 45 ]; do
docker exec "$container" adb logcat -d -s WhoNeedHelpWebView:D '*:S' \
>"$output/webview-current.txt" 2>&1 || true
if grep -Fq "Main-frame load finished: path=/" "$output/webview-current.txt"; then
home_loaded=true
break
fi
attempt=$((attempt + 1))
sleep 2
done
if [ "$home_loaded" != true ]; then
echo "The staging WebView did not finish loading the public home page." >&2
exit 1
fi
docker exec "$container" adb shell uiautomator dump /sdcard/home-window.xml \
>"$output/home-ui-dump-command.txt" 2>&1
docker exec "$container" adb exec-out cat /sdcard/home-window.xml \
>"$output/home-window.xml"
docker exec "$container" adb exec-out screencap -p >"$output/home.png"
docker exec "$container" adb shell am start -W \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "$same_origin" \
-n "$package/$activity" >"$output/safety-start.txt"
safety_loaded=false
attempt=0
while [ "$attempt" -lt 45 ]; do
docker exec "$container" adb logcat -d -s WhoNeedHelpWebView:D '*:S' \
>"$output/webview-current.txt" 2>&1 || true
if grep -Fq "Main-frame load finished: path=/safety" "$output/webview-current.txt"; then
safety_loaded=true
break
fi
attempt=$((attempt + 1))
sleep 2
done
if [ "$safety_loaded" != true ]; then
echo "The same-origin Android deep link did not finish loading /safety." >&2
exit 1
fi
docker exec "$container" adb shell uiautomator dump /sdcard/safety-window.xml \
>"$output/safety-ui-dump-command.txt" 2>&1
docker exec "$container" adb exec-out cat /sdcard/safety-window.xml \
>"$output/safety-window.xml"
docker exec "$container" adb exec-out screencap -p >"$output/safety.png"
docker exec "$container" adb logcat -d >"$output/logcat-before-cleanup.txt"
docker exec "$container" adb shell dumpsys activity activities \
>"$output/activities-before-cleanup.txt"
if grep -Eqi \
'Main-frame load failed|net::ERR_|ERR_CERT|SSL handshake failed|chromium.*crash' \
"$output/logcat-before-cleanup.txt"; then
echo "Android staging logcat contains a public-page load or TLS failure." >&2
exit 1
fi
if ! grep -Fq "$package/$activity" "$output/activities-before-cleanup.txt"; then
echo "The staging activity was not observed after the deep-link load." >&2
exit 1
fi
{
printf 'run_id=%s\n' "$run_id"
printf 'android_api=37.0\n'
printf 'package=%s\n' "$package"
printf 'public_origin=%s\n' "$WNH_BASE_URL"
printf 'home_loaded=true\n'
printf 'same_origin_manifest_filter=true\n'
printf 'same_origin_deep_link_loaded=true\n'
printf 'external_origin_manifest_filter=false\n'
printf 'load_or_tls_errors=0\n'
} >"$output/summary.txt"
echo "Android public staging smoke passed."
echo "Evidence: $output"