git / moonbite-network / mobile/test/android_hardening_test.dart · raw
import 'dart:io';import 'package:flutter_test/flutter_test.dart';/// Repo guards for the Android release ship-blockers. These read the build/// config / manifest / resource files and assert the hardened attributes are/// present, so a regression (e.g. someone re-adds the debug signing key) fails/// CI instead of silently shipping. Paths are relative to the package root,/// which is where `flutter test` runs.File _f(String relative) { final f = File(relative); expect(f.existsSync(), isTrue, reason: 'expected file: $relative'); return f;}void main() { group('release signing (build.gradle.kts)', () { late String gradle; setUpAll(() => gradle = _f('android/app/build.gradle.kts').readAsStringSync()); test('does not sign the release build with the debug key', () { expect( gradle.contains('signingConfigs.getByName("debug")'), isFalse, reason: 'release must not fall back to the debug keystore', ); }); test('wires a real release signing config from key.properties', () { expect(gradle.contains('key.properties'), isTrue); expect(gradle.contains('signingConfigs.getByName("release")'), isTrue); }); test('fails the build when the keystore is absent', () { expect(gradle.contains('GradleException'), isTrue, reason: 'a missing key.properties must hard-fail, not warn'); }); test('uses the MoonBite application id, not the old bigcoin one', () { expect(gradle.contains('org.moonbite.moonbite_mobile'), isTrue); expect(gradle.contains('org.bigcoin.bigcoin_mobile'), isFalse); }); }); group('AndroidManifest hardening', () { late String manifest; setUpAll(() => manifest = _f('android/app/src/main/AndroidManifest.xml').readAsStringSync()); test('disables OS backup of the app sandbox', () { expect(manifest.contains('android:allowBackup="false"'), isTrue); }); test('points at the network security config', () { expect( manifest.contains( 'android:networkSecurityConfig="@xml/network_security_config"'), isTrue, ); }); test('declares the INTERNET permission for the release build', () { expect( manifest.contains('android.permission.INTERNET'), isTrue, ); }); test('declares the biometric permission for the spend gate', () { expect( manifest.contains('android.permission.USE_BIOMETRIC'), isTrue, ); }); }); group('biometric host activity', () { late String kt; setUpAll(() => kt = _f( 'android/app/src/main/kotlin/org/moonbite/moonbite_mobile/MainActivity.kt') .readAsStringSync()); test('MainActivity extends FlutterFragmentActivity for local_auth', () { // local_auth's prompt is a Fragment; a plain FlutterActivity can't host // it and the biometric gate would crash at runtime. expect(kt.contains('FlutterFragmentActivity'), isTrue); expect( kt.contains(': FlutterActivity()'), isFalse, reason: 'must not extend the non-fragment FlutterActivity', ); }); test('sets FLAG_SECURE to block screenshots of seed/balances', () { expect( kt.contains('FLAG_SECURE'), isTrue, reason: 'the recovery phrase and balances must not leak into screen ' 'captures or the recent-apps thumbnail', ); }); }); group('network security config', () { test('forbids cleartext (plain HTTP) traffic', () { final xml = _f('android/app/src/main/res/xml/network_security_config.xml') .readAsStringSync(); expect(xml.contains('cleartextTrafficPermitted="false"'), isTrue); }); }); group('stale bigcoin package', () { test('the old MainActivity.kt is gone', () { expect( File('android/app/src/main/kotlin/org/bigcoin/bigcoin_mobile/MainActivity.kt') .existsSync(), isFalse, ); }); test('the MoonBite MainActivity.kt exists', () { _f('android/app/src/main/kotlin/org/moonbite/moonbite_mobile/MainActivity.kt'); }); });}