diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 399f6981d..b6cf5a6b4 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,7 +1,11 @@ - + + diff --git a/android/app/src/debug/res/xml/network_security_config.xml b/android/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 000000000..6da0e74ae --- /dev/null +++ b/android/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + localhost + 127.0.0.1 + + diff --git a/integration_test/_support/app_setup.dart b/integration_test/_support/app_setup.dart index 81e610978..02a7f4d0f 100644 --- a/integration_test/_support/app_setup.dart +++ b/integration_test/_support/app_setup.dart @@ -16,7 +16,15 @@ import 'package:whitenoise/src/rust/frb_generated.dart'; import '../../test/mocks/mock_secure_storage.dart'; import 'tester_helpers.dart'; -const _relayUrls = ['ws://localhost:8080', 'ws://localhost:7777']; +const _relayUrlsEnv = String.fromEnvironment( + 'WHITENOISE_INTEGRATION_RELAYS', + defaultValue: 'ws://localhost:8080,ws://localhost:7777', +); +final List _relayUrls = _relayUrlsEnv + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); bool _rustBridgeInitialized = false; Directory? _backendRoot; @@ -78,21 +86,35 @@ Future mountApp(WidgetTester tester) async { } Future expectLocalRelaysAvailable() async { - await _expectLocalRelayAvailable(8080); - await _expectLocalRelayAvailable(7777); + for (final url in _relayUrls) { + final uri = Uri.tryParse(url); + if (uri == null) { + fail( + 'Unparseable relay URL "$url" in WHITENOISE_INTEGRATION_RELAYS. ' + 'Fix the relay list (comma-separated) and run the test again.', + ); + } + final host = uri.host; + final isLocal = host == 'localhost' || host == '127.0.0.1'; + if (!isLocal) continue; + final port = uri.hasPort + ? uri.port + : (uri.scheme == 'wss' ? 443 : 80); + await _expectLocalRelayAvailable(host, port); + } } -Future _expectLocalRelayAvailable(int port) async { +Future _expectLocalRelayAvailable(String host, int port) async { try { final socket = await Socket.connect( - '127.0.0.1', + host, port, timeout: const Duration(seconds: 1), ); socket.destroy(); } catch (error) { fail( - 'Expected a local Nostr relay on 127.0.0.1:$port before running this integration test. ' + 'Expected a local Nostr relay on $host:$port before running this integration test. ' 'Run `docker compose up -d`, then run the test again. ' 'Connection error: $error', ); diff --git a/justfile b/justfile index bfe4c40ec..28a9dfb69 100644 --- a/justfile +++ b/justfile @@ -168,51 +168,85 @@ test-flutter-quiet: echo "No test directory found."; \ fi -# Resolves the integration-test device: the given id, else the one booted simulator. +# Resolves the integration-test device: the given id, else the one booted iOS simulator or Android device. _resolve-device device: @device="{{ device }}"; \ if [ -n "$device" ]; then echo "$device"; exit 0; fi; \ - booted=$(xcrun simctl list devices booted 2>/dev/null | grep -oiE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}'); \ - count=$(printf '%s' "$booted" | grep -c .); \ - if [ "$count" -eq 1 ]; then \ - echo "Using booted simulator $booted" >&2; \ - echo "$booted"; \ - elif [ "$count" -eq 0 ]; then \ - echo "No device id given and no booted simulator found. Boot one, pass a device id, or set WHITENOISE_INTEGRATION_DEVICE." >&2; \ + if command -v xcrun >/dev/null 2>&1; then \ + booted=$(xcrun simctl list devices booted 2>/dev/null | grep -oiE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}'); \ + else \ + booted=""; \ + fi; \ + ios_count=$(printf '%s' "$booted" | grep -c .); \ + if command -v adb >/dev/null 2>&1; then \ + android=$(adb devices 2>/dev/null | awk 'NR>1 && $2=="device" {print $1}'); \ + else \ + android=""; \ + fi; \ + android_count=$(printf '%s' "$android" | grep -c .); \ + total=$((ios_count + android_count)); \ + if [ "$total" -eq 1 ]; then \ + picked="${booted}${android}"; \ + echo "Using device $picked" >&2; \ + echo "$picked"; \ + elif [ "$total" -eq 0 ]; then \ + echo "No device id given and no booted iOS simulator or Android device found. Boot one, pass a device id, or set WHITENOISE_INTEGRATION_DEVICE." >&2; \ exit 1; \ else \ - echo "Multiple booted simulators โ€” pass a device id or set WHITENOISE_INTEGRATION_DEVICE:" >&2; \ - xcrun simctl list devices booted >&2; \ + echo "Multiple devices found โ€” pass a device id or set WHITENOISE_INTEGRATION_DEVICE:" >&2; \ + [ -n "$booted" ] && echo "iOS simulators:" >&2 && echo "$booted" >&2; \ + [ -n "$android" ] && echo "Android devices:" >&2 && echo "$android" >&2; \ exit 1; \ fi -# Run Flutter integration tests. Requires local Nostr relays on ports 8080 and 7777. +# If device is an Android adb device, tunnel host ports 8080/7777 to its loopback via +# `adb reverse` so `ws://localhost:` from the device hits the host's docker relays. +# No-op for iOS simulators and desktop targets. +_setup-android-relays device: + @if command -v adb >/dev/null 2>&1 && adb devices 2>/dev/null | awk 'NR>1 && $2=="device" {print $1}' | grep -Fqx "{{ device }}"; then \ + adb -s "{{ device }}" reverse tcp:8080 tcp:8080 >/dev/null || { echo "โŒ adb reverse tcp:8080 failed for {{ device }}" >&2; exit 1; }; \ + adb -s "{{ device }}" reverse tcp:7777 tcp:7777 >/dev/null || { echo "โŒ adb reverse tcp:7777 failed for {{ device }}" >&2; exit 1; }; \ + echo "๐Ÿ“ก Android device {{ device }} โ€” adb reverse set for ports 8080, 7777 โ†’ host docker relays" >&2; \ + fi + +# Run Flutter integration tests against local Nostr relays on ports 8080 and 7777 +# (`docker compose up -d`). On Android, `adb reverse` tunnels the device's localhost +# to the host so the same URLs work on iOS simulators, desktop, and Android. # Run one file by passing its path: `just int-test integration_test/messaging_interactions_test.dart`. -# Device: WHITENOISE_INTEGRATION_DEVICE, else the one booted simulator. +# Device: WHITENOISE_INTEGRATION_DEVICE, else the one booted simulator/emulator. +# Relays: WHITENOISE_INTEGRATION_RELAYS (comma-separated) overrides defaults. int-test target="integration_test/all_tests.dart" device=env("WHITENOISE_INTEGRATION_DEVICE", "") flavor="staging": @echo "๐Ÿงช Testing Flutter integration flows..." @device=$(just _resolve-device "{{ device }}") || exit 1; \ + just _setup-android-relays "$device" || exit 1; \ + define=""; \ + [ -n "${WHITENOISE_INTEGRATION_RELAYS:-}" ] && define="--dart-define=WHITENOISE_INTEGRATION_RELAYS=$WHITENOISE_INTEGRATION_RELAYS"; \ if [ -n "{{ flavor }}" ]; then \ - flutter test -d "$device" --flavor {{ flavor }} {{ target }}; \ + flutter test -d "$device" --flavor {{ flavor }} ${define:+"$define"} {{ target }}; \ else \ - flutter test -d "$device" {{ target }}; \ + flutter test -d "$device" ${define:+"$define"} {{ target }}; \ fi -# Run Flutter integration tests with minimal output. Requires local Nostr relays on ports 8080 and 7777. +# Run Flutter integration tests with minimal output against local Nostr relays on +# ports 8080 and 7777; Android tunnels via `adb reverse`. # Run one file by passing its path: `just int-test-quiet integration_test/messaging_interactions_test.dart`. -# Device: WHITENOISE_INTEGRATION_DEVICE, else the one booted simulator. +# Device: WHITENOISE_INTEGRATION_DEVICE, else the one booted simulator/emulator. +# Relays: WHITENOISE_INTEGRATION_RELAYS (comma-separated) overrides defaults. int-test-quiet target="integration_test/all_tests.dart" device=env("WHITENOISE_INTEGRATION_DEVICE", "") flavor="staging": @if [ ! -e "{{ target }}" ]; then \ echo "No integration test target found at {{ target }}."; \ exit 1; \ fi; \ device=$(just _resolve-device "{{ device }}") || exit 1; \ + just _setup-android-relays "$device" || exit 1; \ + define=""; \ + [ -n "${WHITENOISE_INTEGRATION_RELAYS:-}" ] && define="--dart-define=WHITENOISE_INTEGRATION_RELAYS=$WHITENOISE_INTEGRATION_RELAYS"; \ if [ -n "{{ flavor }}" ]; then \ - flutter test -d "$device" --flavor {{ flavor }} --no-pub --reporter=failures-only {{ target }}; \ + flutter test -d "$device" --flavor {{ flavor }} --no-pub --reporter=failures-only ${define:+"$define"} {{ target }}; \ else \ - flutter test -d "$device" --no-pub --reporter=failures-only {{ target }}; \ + flutter test -d "$device" --no-pub --reporter=failures-only ${define:+"$define"} {{ target }}; \ fi coverage min="99":