taler-monitoring/android-test/run-android-pay-smoke.sh
Hernâni Marques 5a5431bfa9
taler-monitoring v1.13.12
Multi-phase global SUMMARY, warn-filter dimmed context, SUMMARY chrome
as meta; FP stage host-agent via francpaysan-stage-user (stagepaysan);
monpages GOA + FP stage.
2026-07-19 04:22:10 +02:00

308 lines
12 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Android wallet smoke: install APK, then **withdraw (Abheben)** and **pay** via
# deep-link + best-effort UI taps. Both legs are first-class (not pay-only).
#
# Flow (default both on):
# 1) WITHDRAW taler://withdraw/… from bank demo-withdraw.json + confirm taps
# 2) PAY taler://pay… / pay-template + optional taps
#
# Env:
# DO_WITHDRAW=1 (default) DO_PAY=1 (default)
# SMOKE_STRICT=1 → require wallet-core withdraw (+ pay if DO_PAY) + no ANR
# WITHDRAW_GUI_ROUNDS multi-round Abheben taps (default 6)
#
# Usage:
# ./android-test/run-android-pay-smoke.sh
# STACK=goa ./android-test/run-android-pay-smoke.sh
# DO_PAY=0 ./android-test/run-android-pay-smoke.sh # withdraw-only
#
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib_android_env.sh
. "$ROOT/lib_android_env.sh"
if [ "${AUTO_ANDROID}" != "1" ]; then
echo "skipped: AUTO_ANDROID=${AUTO_ANDROID} (platform flags — see GUI-AUTOMATION-NOTES.md)"
exit 0
fi
APK_DIR="${APK_DIR:-$ROOT/apks}"
APK_URL="${APK_URL:-https://f-droid.org/repo/net.taler.wallet.fdroid_854.apk}"
APK_NAME="${APK_NAME:-net.taler.wallet.fdroid_854.apk}"
# Optional override: full path to an already-built APK (from-source)
APK_PATH="${APK_PATH:-}"
PKG="${PKG:-net.taler.wallet.fdroid}"
STACK="${STACK:-auto}" # auto | goa | stage
SERIAL="${SERIAL:-}"
OUT_DIR="${OUT_DIR:-$ROOT/out}"
mkdir -p "$APK_DIR" "$OUT_DIR"
if ! command -v adb >/dev/null 2>&1; then
echo "adb missing — install android-tools-adb or user SDK platform-tools" >&2
exit 2
fi
# Default: headless AVD (no host window). WINDOWED=1 / EMULATOR_HEADLESS=0 for UI.
if ! android_ensure_device; then
cat >&2 <<EOF
No adb device. Options:
1) Plug phone with USB debugging
2) Headless AVD (default): $ROOT/start-android-emulator.sh --wait
3) Windowed AVD: WINDOWED=1 $ROOT/start-android-emulator.sh --wait
4) AUTO_START_EMULATOR=0 disables auto-start from this smoke
EOF
exit 3
fi
ADB=(adb -s "$SERIAL")
echo "device: $SERIAL emulator_headless=${EMULATOR_HEADLESS} gpu=${EMULATOR_GPU}"
# Resolve stack endpoints
case "$STACK" in
auto)
if curl -sfS -m 5 -o /dev/null https://bank.hacktivism.ch/config 2>/dev/null; then
STACK=goa
else
STACK=stage
fi
;;
esac
case "$STACK" in
goa)
BANK=https://bank.hacktivism.ch
MERCHANT=https://taler.hacktivism.ch
WITHDRAW_JSON="$BANK/intro/demo-withdraw.json"
PAY_HINT="paivana template / goa-shop"
;;
stage)
BANK=https://stage.bank.lefrancpaysan.ch
MERCHANT=https://stage.monnaie.lefrancpaysan.ch
WITHDRAW_JSON="$BANK/intro/demo-withdraw.json"
PAY_HINT="farmer shop templates"
;;
*) echo "unknown STACK=$STACK" >&2; exit 2 ;;
esac
echo "stack: $STACK bank: $BANK"
# APK: explicit path, or download published F-Droid into APK_DIR
if [ -n "$APK_PATH" ] && [ -f "$APK_PATH" ]; then
:
elif [ -f "$APK_DIR/$APK_NAME" ]; then
APK_PATH="$APK_DIR/$APK_NAME"
else
APK_PATH="$APK_DIR/$APK_NAME"
echo "downloading published wallet APK…"
curl -fL --retry 3 -o "$APK_PATH" "$APK_URL"
fi
echo "apk: $APK_PATH ($(wc -c <"$APK_PATH") bytes)"
echo "pkg: $PKG"
: "${DO_WITHDRAW:=1}"
: "${DO_PAY:=1}"
: "${WITHDRAW_GUI_ROUNDS:=6}"
: "${PAY_GUI_ROUNDS:=4}"
: "${SMOKE_STRICT:=0}"
"${ADB[@]}" wait-for-device
"${ADB[@]}" install -r "$APK_PATH"
"${ADB[@]}" shell settings put global window_animation_scale 0 || true
"${ADB[@]}" shell settings put global transition_animation_scale 0 || true
"${ADB[@]}" shell settings put global animator_duration_scale 0 || true
"${ADB[@]}" shell am force-stop "$PKG" 2>/dev/null || true
"${ADB[@]}" logcat -c 2>/dev/null || true
WURI=""
PAY_URI=""
WITHDRAW_OK=0
PAY_OK=0
# ═══════════════════════════════════════════════════════════════
# 1) WITHDRAW / Abheben (required by default — not pay-only)
# ═══════════════════════════════════════════════════════════════
if [ "$DO_WITHDRAW" = "1" ]; then
echo
echo "======== 1 WITHDRAW (Abheben) ========"
WURI=$(curl -sfS -m 25 "$WITHDRAW_JSON" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("taler_withdraw_uri") or d.get("qr_payload") or "")')
if [ -z "$WURI" ]; then
echo "FAIL: no taler_withdraw_uri from $WITHDRAW_JSON" >&2
exit 4
fi
echo "withdraw URI: $WURI"
echo "$WURI" >"$OUT_DIR/last-withdraw.uri"
"${ADB[@]}" shell am start -a android.intent.action.VIEW -d "$WURI" 2>&1 | tee "$OUT_DIR/start-withdraw.txt"
sleep 6
"${ADB[@]}" exec-out screencap -p >"$OUT_DIR/01-after-withdraw-uri.png" || true
"${ADB[@]}" shell uiautomator dump /sdcard/ui.xml 2>/dev/null || true
"${ADB[@]}" pull /sdcard/ui.xml "$OUT_DIR/01-ui.xml" 2>/dev/null || true
# Multi-round confirm / Abheben / ToS (same idea as GUI smoke)
if [ -f "$ROOT/lib_ui.py" ]; then
python3 - "$ROOT/lib_ui.py" "$SERIAL" "$OUT_DIR" "$WITHDRAW_GUI_ROUNDS" 2 <<'PY' || true
import json, sys, importlib.util
from pathlib import Path
lib_path, serial, out, rounds, sleep_s = (
Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
)
spec = importlib.util.spec_from_file_location("lib_ui", lib_path)
lib = importlib.util.module_from_spec(spec)
spec.loader.exec_module(lib)
st = lib.gui_drive(serial, out / "gui-withdraw", rounds=rounds, sleep_s=sleep_s, phase="wd")
(out / "gui-withdraw-status.json").write_text(json.dumps(st, indent=2))
print("withdraw GUI taps:", st.get("taps"), "anr:", st.get("anr_dismissals"))
PY
else
python3 - "$SERIAL" "$OUT_DIR/01-ui.xml" <<'PY' || true
import re, subprocess, sys
serial, path = sys.argv[1], sys.argv[2]
try:
xml = open(path, errors="replace").read()
except FileNotFoundError:
raise SystemExit(0)
labels = [
"Confirm", "Confirm withdrawal", "Withdraw", "Accept", "Continue", "OK",
"I accept", "Agree", "Bestätigen", "Abheben", "Akzeptieren", "Retirer",
]
for label in labels:
for pat in (
r'text="%s"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"' % re.escape(label),
r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"[^>]*text="%s"' % re.escape(label),
):
for m in re.finditer(pat, xml, flags=re.I):
x1, y1, x2, y2 = map(int, m.groups())
x, y = (x1 + x2) // 2, (y1 + y2) // 2
print(f"tap {label} @ {x},{y}")
subprocess.run(["adb", "-s", serial, "shell", "input", "tap", str(x), str(y)], check=False)
PY
sleep 8
fi
"${ADB[@]}" exec-out screencap -p >"$OUT_DIR/02-after-withdraw-taps.png" || true
# Mid-flow logcat: withdraw must register before pay
"${ADB[@]}" logcat -d -t 400 >"$OUT_DIR/logcat-after-withdraw.txt" || true
if grep -qE 'prepareBankIntegratedWithdrawal|acceptWithdrawal|confirmWithdrawal' \
"$OUT_DIR/logcat-after-withdraw.txt" 2>/dev/null; then
WITHDRAW_OK=1
echo "withdraw: wallet-core activity YES"
else
echo "withdraw: wallet-core activity NOT seen yet (onboarding/ANR/slow?)"
fi
else
echo "======== 1 WITHDRAW skipped (DO_WITHDRAW=0) ========"
fi
# ═══════════════════════════════════════════════════════════════
# 2) PAY (after withdraw; still first-class, but second leg)
# ═══════════════════════════════════════════════════════════════
if [ "$DO_PAY" = "1" ]; then
echo
echo "======== 2 PAY (Bezahlen) ========"
if [ "$STACK" = "stage" ]; then
if RESP=$(curl -sfS -m 20 -X POST -H 'Content-Type: application/json' -d '{}' \
"$MERCHANT/instances/fermes-des-collines/templates/panier-legumes" 2>/dev/null); then
PAY_URI=$(printf '%s' "$RESP" | python3 -c '
import json,sys
d=json.load(sys.stdin)
oid=d.get("order_id") or ""
tok=d.get("token") or ""
if oid and tok:
print("taler://pay/stage.monnaie.lefrancpaysan.ch/instances/fermes-des-collines/%s/?c=%s"%(oid,tok))
' 2>/dev/null || true)
fi
elif [ "$STACK" = "goa" ]; then
PAY_URI="taler://pay-template/taler.hacktivism.ch/instances/goa-shop/paivana"
fi
if [ -n "$PAY_URI" ]; then
echo "pay URI: $PAY_URI ($PAY_HINT)"
echo "$PAY_URI" >"$OUT_DIR/last-pay.uri"
"${ADB[@]}" shell am start -a android.intent.action.VIEW -d "$PAY_URI" 2>&1 | tee "$OUT_DIR/start-pay.txt"
sleep 6
if [ -f "$ROOT/lib_ui.py" ]; then
python3 - "$ROOT/lib_ui.py" "$SERIAL" "$OUT_DIR" "$PAY_GUI_ROUNDS" 2 <<'PY' || true
import json, sys, importlib.util
from pathlib import Path
lib_path, serial, out, rounds, sleep_s = (
Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
)
spec = importlib.util.spec_from_file_location("lib_ui", lib_path)
lib = importlib.util.module_from_spec(spec)
spec.loader.exec_module(lib)
st = lib.gui_drive(serial, out / "gui-pay", rounds=rounds, sleep_s=sleep_s, phase="pay")
(out / "gui-pay-status.json").write_text(json.dumps(st, indent=2))
print("pay GUI taps:", st.get("taps"), "anr:", st.get("anr_dismissals"))
PY
fi
"${ADB[@]}" exec-out screencap -p >"$OUT_DIR/03-after-pay-uri.png" || true
else
echo "pay: (no public template URI for stack=$STACK)"
fi
else
echo "======== 2 PAY skipped (DO_PAY=0) ========"
fi
# Final evidence
"${ADB[@]}" logcat -d -t 500 >"$OUT_DIR/logcat.txt" || true
grep -iE 'taler-wallet|prepareBank|acceptWithdrawal|confirmWithdrawal|preparePay|confirmPay|error|Error|success|withdraw|pay' \
"$OUT_DIR/logcat.txt" | tail -100 >"$OUT_DIR/logcat-taler.txt" || true
# Re-evaluate withdraw/pay from full logcat
if [ "$DO_WITHDRAW" = "1" ]; then
if grep -qE 'prepareBankIntegratedWithdrawal|acceptWithdrawal|confirmWithdrawal' \
"$OUT_DIR/logcat.txt" 2>/dev/null; then
WITHDRAW_OK=1
fi
fi
if [ "$DO_PAY" = "1" ] && [ -n "$PAY_URI" ]; then
if grep -qiE 'preparePay|confirmPay|preparePurchase|checkPay|pay-template|PayForTemplate' \
"$OUT_DIR/logcat.txt" 2>/dev/null; then
PAY_OK=1
fi
fi
rc=0
echo
echo "=== summary (withdraw + pay) ==="
echo "package: $PKG stack: $STACK"
echo "DO_WITHDRAW=$DO_WITHDRAW DO_PAY=$DO_PAY SMOKE_STRICT=$SMOKE_STRICT"
if [ "$DO_WITHDRAW" = "1" ]; then
echo "withdraw URI: ${WURI:-(none)}"
if [ "$WITHDRAW_OK" = "1" ]; then
echo "withdraw wallet-core: YES"
else
echo "withdraw wallet-core: NO"
if [ "$SMOKE_STRICT" = "1" ]; then
echo "FAIL: SMOKE_STRICT requires withdraw (prepareBank/accept/confirm) in logcat"
rc=11
fi
fi
fi
if [ "$DO_PAY" = "1" ]; then
echo "pay URI: ${PAY_URI:-(none)}"
if [ -z "$PAY_URI" ]; then
echo "pay: no URI (template/API)"
if [ "$SMOKE_STRICT" = "1" ]; then
echo "FAIL: SMOKE_STRICT requires a pay URI when DO_PAY=1"
rc=12
fi
elif [ "$PAY_OK" = "1" ]; then
echo "pay wallet-core: YES"
else
echo "pay wallet-core: NO (intent may still have been delivered)"
if [ "$SMOKE_STRICT" = "1" ]; then
echo "FAIL: SMOKE_STRICT requires pay path activity in logcat"
[ "$rc" -eq 0 ] && rc=13
fi
fi
fi
if grep -qiE "isn't responding|isn.t responding|System UI isn" "$OUT_DIR/01-ui.xml" 2>/dev/null \
|| grep -qiE "isn't responding|not responding" "$OUT_DIR/logcat.txt" 2>/dev/null \
|| grep -rqiE "isn't responding" "$OUT_DIR/gui-withdraw" 2>/dev/null; then
echo "FAIL: System UI / app ANR"
rc=10
fi
echo "artifacts: $OUT_DIR/"
ls -la "$OUT_DIR" | sed 's/^/ /'
echo
echo "NOTE: Full unattended Abheben+Bezahlen needs responsive device (≥68GiB host RAM)."
echo "Smoke legs: withdraw=$( [ "$DO_WITHDRAW" = 1 ] && echo on || echo off) pay=$( [ "$DO_PAY" = 1 ] && echo on || echo off) rc=$rc"
exit "$rc"