build: add reproducible Android release verification

Pin Gradle inputs and dependency metadata, then enforce analyze, lint, build, R8, immutability, and regression gates in CI.
This commit is contained in:
Naveax committed 2026-09-04 00:08:57 +03:00
1 parent c9826c15a0
commit 7e66109e8d
11 files changed
+9788 -8

No files matched your search

@@ -0,0 +1,98 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('native runtime is generation-scoped, readiness-gated and deadline-bound', () {
const root = 'local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless';
final plugin = File('$root/FlutterVlessPlugin.kt').readAsStringSync();
final config = File('$root/xray/dto/XrayConfig.kt').readAsStringSync();
final service = File('$root/xray/service/XrayVPNService.kt').readAsStringSync();
final core = File('$root/xray/core/XrayCoreManager.kt').readAsStringSync();
final timer = File('lib/logic/session_timer.dart').readAsStringSync();
final vpn = File('lib/logic/vpn_connection.dart').readAsStringSync();
final hivemind = File('lib/logic/hivemind_service.dart').readAsStringSync();
final main = File('lib/main.dart').readAsStringSync();
final mainActivity = File(
'android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt',
).readAsStringSync();
final support = File('lib/components/support_button.dart').readAsStringSync();
final connectionSettings =
File('lib/logic/connection_settings.dart').readAsStringSync();
expect(config, contains('var RUNTIME_TOKEN: String = ""'));
expect(plugin, contains('expectedRuntimeToken'));
expect(plugin, contains('runtimeToken != currentToken'));
expect(plugin, contains('runtimeReady'));
expect(plugin, contains('STOP_TIMEOUT'));
expect(plugin, contains('setSessionDeadline'));
expect(plugin, contains('result.error("NO_ACTIVITY"'));
expect(plugin, isNot(contains('activity!!')));
expect(plugin, contains('if (token == null) {'));
expect(plugin, contains('intent.putExtra("RUNTIME_TOKEN", token)'));
expect(
plugin,
isNot(contains('if (token != null) intent.putExtra("RUNTIME_TOKEN", token)')),
);
expect(plugin, isNot(contains('"getServerDelay" ->')));
expect(plugin, isNot(contains('"getConnectedServerDelay" ->')));
expect(
service,
contains('val requestedToken = intent.getStringExtra("RUNTIME_TOKEN").orEmpty()'),
);
expect(service, contains('requestedToken.isEmpty() ||'));
expect(service, contains('requestedToken != activeToken'));
expect(service, contains('stopAll(requestedToken)'));
expect(hivemind, contains('..followRedirects = false;'));
expect(plugin, contains('failPendingPermission("ENGINE_DETACHED"'));
expect(plugin, contains('failPendingPermission("NO_ACTIVITY"'));
expect(service, contains('BOOTSTRAP_SESSION_SECONDS = 120L'));
expect(service, contains('UPDATE_SESSION_DEADLINE'));
expect(service, contains('SystemClock.elapsedRealtime()'));
expect(service, contains('AlarmManager.RTC_WAKEUP'));
expect(service, contains('setExactAndAllowWhileIdle'));
expect(service, contains('setAndAllowWhileIdle'));
expect(service, contains('PREF_EXPIRES_AT_MS'));
expect(service, contains('PREF_RUNTIME_TOKEN'));
expect(service, contains('markRuntimeReady(this, config)'));
expect(service, contains('if (currentConfig == null) stopSelf()'));
expect(service, isNot(contains('sessionDeadlineEpochMs')));
expect(service, isNot(contains('active_session.bin')));
expect(service, isNot(contains('ObjectOutputStream')));
expect(core, contains('AppConfigs.RUNTIME_READY = false'));
expect(core, contains('fun markRuntimeReady'));
expect(timer, contains('setNativeSessionDeadline(_remainingAtLastSync)'));
expect(timer, contains('Future.microtask(_onVpnConnectionChanged)'));
expect(timer, contains('Completer<void>? _syncCompletion'));
expect(timer, contains('await active.future;'));
expect(timer, contains('_sessionEpoch++;'));
expect(timer, contains('_supportStateEpoch++;'));
expect(timer, contains('_isDisconnecting = true;'));
expect(vpn, contains('bool _disposed = false;'));
expect(vpn, contains('if (_disposed) return;'));
expect(vpn, contains('_connectEpoch++;'));
expect(vpn, contains('HivemindService.cancel();'));
expect(main, contains('ChangeNotifierProxyProvider<VpnConnection, SessionTimer>('));
expect(main, contains('lazy: false'));
expect(support, contains('await timer.syncNow();'));
expect(vpn, contains("MethodChannel('flutter_vless')"));
expect(mainActivity, contains('manager.activeNotifications'));
expect(mainActivity, contains('serviceStopAction'));
expect(mainActivity, isNot(contains('V2RAY_SERVICE_COMMANDS.STOP_SERVICE')));
expect(mainActivity, isNot(contains('XrayVPNService::class.java')));
expect(
connectionSettings,
contains('storedMode == ConnectionMode.proxy.name'),
);
expect(connectionSettings, isNot(contains("storedMode == 'ass'")));
});
}
@@ -0,0 +1,58 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('notification updates do not trust ActivityManager process visibility', () {
final source = File(
'android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt',
).readAsStringSync();
expect(source, isNot(contains('ActivityManager')));
expect(source, isNot(contains('isVpnServiceProcessAlive')));
expect(source, isNot(contains('VPN_PROCESS_SUFFIX')));
expect(source, contains('NotificationManagerCompat.from(this).notify'));
});
test('SOCKS reads use one total deadline instead of per-chunk timeout', () {
final source = File('lib/logic/local_socks_tester.dart').readAsStringSync();
expect(
source,
contains('timeout.inMicroseconds - elapsed.elapsedMicroseconds'),
);
expect(
source,
contains("throw TimeoutException('Socket read timed out', timeout);"),
);
});
test('Hivemind captures cancellation generation before first async lookup', () {
final source = File('lib/logic/hivemind_service.dart').readAsStringSync();
final callIdIndex = source.indexOf('final callId = ++_currentCallId;');
final deviceIdIndex =
source.indexOf('final deviceId = await CryptoService.getDeviceId();');
expect(callIdIndex, greaterThanOrEqualTo(0));
expect(deviceIdIndex, greaterThan(callIdIndex));
expect(
source.substring(deviceIdIndex),
contains('_throwIfCancelled(callId);'),
);
expect(
source,
contains('_currentCallId++;\n _expectedNonce = null;'),
);
});
test('notification text is cached only after native update succeeds', () {
final source =
File('lib/logic/notification_service.dart').readAsStringSync();
final invokeIndex =
source.indexOf("await _channel.invokeMethod<void>('updateNotificationText'");
final cacheIndex = source.indexOf('_lastText = text;');
expect(invokeIndex, greaterThanOrEqualTo(0));
expect(cacheIndex, greaterThan(invokeIndex));
});
}
+230
View File
@@ -0,0 +1,230 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:revoltvpn/logic/secure_socks_session.dart';
void main() {
test('secure SOCKS session owns the entire inbound surface', () async {
final session = await SecureSocksSession.create(jsonEncode({
'inbounds': [
{
'tag': 'remote-listener',
'listen': '0.0.0.0',
'port': 9999,
'protocol': 'dokodemo-door',
},
{
'tag': 'old-http',
'listen': '127.0.0.1',
'port': 8080,
'protocol': 'http',
},
],
'outbounds': [
{'tag': 'proxy', 'protocol': 'freedom'}
],
}));
final decoded = jsonDecode(session.configJson) as Map<String, dynamic>;
final inbounds = decoded['inbounds'] as List<dynamic>;
expect(inbounds, hasLength(1));
final inbound = inbounds.single as Map<String, dynamic>;
expect(inbound['tag'], SecureSocksSession.inboundTag);
expect(inbound['listen'], '127.0.0.1');
expect(inbound['protocol'], 'socks');
expect(inbound['port'], session.port);
final settings = inbound['settings'] as Map<String, dynamic>;
expect(settings['auth'], 'password');
final users = settings['users'] as List<dynamic>;
expect(users, hasLength(1));
final account = users.single as Map<String, dynamic>;
expect(account['user'], session.username);
expect(account['pass'], session.password);
});
test('vendored native source keeps full-tunnel and private IPC invariants', () {
const root =
'local_packages/flutter_vless_android-1.1.5/android/src/main';
final service = File(
'$root/kotlin/com/github/tfox/flutter_vless/xray/service/XrayVPNService.kt',
).readAsStringSync();
final core = File(
'$root/kotlin/com/github/tfox/flutter_vless/xray/core/XrayCoreManager.kt',
).readAsStringSync();
final plugin = File(
'$root/kotlin/com/github/tfox/flutter_vless/FlutterVlessPlugin.kt',
).readAsStringSync();
final manifest = File('$root/AndroidManifest.xml').readAsStringSync();
expect(service, contains('builder.addRoute("0.0.0.0", 0)'));
expect(service, contains('builder.addRoute("::", 0)'));
expect(service, contains('Android refused to establish VPN interface'));
expect(service, contains('Per-app VPN bypass is disabled in ReVolt'));
expect(service, contains('Failed to configure VPN DNS'));
expect(service, contains('reader.forEachLine { _ -> }'));
expect(plugin, contains('ContextCompat.RECEIVER_NOT_EXPORTED'));
expect(core, contains('.setPackage(context.packageName)'));
expect(core, contains('REVOLT_VPN_SERVICE'));
expect(core, contains('NotificationManager.IMPORTANCE_LOW'));
expect(core, contains('deleteEphemeralConfigOnFailure'));
expect(core, contains('reader.forEachLine { _ -> }'));
expect(manifest, contains('android:exported="false"'));
expect(manifest, contains('android:permission="android.permission.BIND_VPN_SERVICE"'));
expect(manifest, contains('android.net.VpnService.SUPPORTS_ALWAYS_ON'));
expect(manifest, contains('android:value="false"'));
expect(manifest, isNot(contains('android.permission.CHANGE_NETWORK_STATE')));
expect(manifest, isNot(contains('android.permission.READ_EXTERNAL_STORAGE')));
});
test('native app bridge keeps runtime adoption on private heartbeat only', () {
final source = File(
'android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt',
).readAsStringSync();
final vpnSource = File('lib/logic/vpn_connection.dart').readAsStringSync();
final powerSource = File('lib/logic/power_settings.dart').readAsStringSync();
expect(source, isNot(contains('openVpnSettings')));
expect(source, isNot(contains('ACTION_VPN_SETTINGS')));
expect(source, isNot(contains('FROM_DISCONNECT_BTN')));
expect(source, isNot(contains('isVpnRuntimeAlive')));
expect(source, isNot(contains('isVpnServiceProcessAlive')));
expect(source, contains('REVOLT_VPN_SERVICE'));
expect(vpnSource, contains('if (_connectEpoch == 0) _adoptedRunningRuntime = true;'));
expect(vpnSource, isNot(contains('PowerSettings.runtimeState')));
expect(vpnSource, isNot(contains('_adoptRunningRuntime')));
expect(powerSource, isNot(contains('runtimeState')));
expect(powerSource, isNot(contains('VpnRuntimeState')));
});
test('configured AdMob bypass contract remains intact', () {
final source = File('lib/logic/hivemind_service.dart').readAsStringSync();
expect(source, contains('await _runConfiguredBypass(deviceId, nonce);'));
expect(
source,
contains('/admob/callback?signature=test&key_id=test&custom_data='),
);
});
test('local SOCKS diagnostics cannot fall back to unauthenticated legacy mode', () {
final source = File('lib/logic/local_socks_tester.dart').readAsStringSync();
expect(source, contains('required int port'));
expect(source, contains('required String username'));
expect(source, contains('required String password'));
expect(source, contains('const <int>[0x05, 0x01, 0x02]'));
expect(source, isNot(contains('const <int>[0x05, 0x01, 0x00]')));
expect(source, isNot(contains('int port = 10807')));
expect(source, isNot(contains('paladinvpn.duckdns.org')));
expect(source, contains("String targetHost = 'example.com'"));
expect(source, contains('const totalTimeout = Duration(seconds: 8);'));
});
test('hivemind rejects missing nonce and stale session responses', () {
final source = File('lib/logic/hivemind_service.dart').readAsStringSync();
expect(source, contains('_expectedNonce != nonce ||'));
expect(source, contains('serverNonce is! String ||'));
expect(
source,
contains(
'final session = await _fetchActiveSession(deviceId, nonce);\n'
' _throwIfCancelled(callId);',
),
);
});
test('hivemind validates and pins session fields before constructing VLESS', () {
final source = File('lib/logic/hivemind_service.dart').readAsStringSync();
expect(source, contains('_uuidPattern'));
expect(source, contains('_maxControlResponseBytes = 256 * 1024'));
expect(source, contains('controlGet('));
expect(source, contains('_validatedPublicIp(AppConfig.serverIp'));
expect(source, contains('const port = 443;'));
expect(source, contains("_requiredString(json, 'reality_pbk', maxLength: 128)"));
expect(source, contains('_validatedHost('));
expect(
source,
contains("final pinnedHost = _validatedPublicIp(AppConfig.serverIp, 'serverIp');"),
);
expect(source, contains('advertisedHost.trim() != pinnedHost'));
expect(
source,
isNot(contains("_stringOr(json['vless_ip'], AppConfig.serverIp)")),
);
expect(source, contains('return Uri('));
expect(source, contains('queryParameters: <String, String>{'));
expect(source, isNot(contains("return 'vless://\$uuid@\$host:\$port'")));
});
test('VPN connect work cannot outlive its generation', () {
final source = File('lib/logic/vpn_connection.dart').readAsStringSync();
expect(source, contains('int _connectEpoch = 0;'));
expect(source, contains('final connectEpoch = ++_connectEpoch;'));
expect(source, contains('epoch == _connectEpoch && !_userDisconnecting'));
expect(source, contains('_connectEpoch++;\n _suppressNativeConnect = true;'));
expect(source, contains('if (_suppressNativeConnect || _userDisconnecting) return;'));
expect(source, isNot(contains('bool _cancelled = false;')));
});
test('native recovery is fail-closed without hot process restart loops', () {
const root =
'local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless';
final service =
File('$root/xray/service/XrayVPNService.kt').readAsStringSync();
final core = File('$root/xray/core/XrayCoreManager.kt').readAsStringSync();
expect(service, contains('scheduleTun2socksRecovery'));
expect(service, contains('tun2socksRecoveryAttempt'));
expect(service, contains('coerceAtMost(30_000L)'));
expect(service, contains('recoveringTun2socks = false'));
expect(service, contains('tun2socksRecoveryAttempt = 0'));
expect(service, contains('XrayCoreManager.markRuntimeConnecting(this, config)'));
expect(service, isNot(contains('Thread.sleep(350)')));
expect(service, isNot(contains('Thread.sleep(600)')));
expect(
service,
isNot(contains('attempt = 0\n Thread.sleep(3000)')),
);
expect(core, contains('context.handleXrayCoreExit(config)'));
expect(core, contains('AppConfigs.RUNTIME_READY = false'));
});
test('data disclosure only persists an explicit acknowledged action', () {
final source =
File('lib/components/data_disclosure_dialog.dart').readAsStringSync();
expect(source, contains('final accepted = await showDialog<bool>('));
expect(source, contains('if (accepted != true) return;'));
expect(source, contains('final saved = await prefs.setBool(_key, true);'));
expect(source, contains('if (!saved)'));
expect(source, contains('Navigator.of(context).pop(true)'));
expect(source, isNot(contains('prefs.setBool(_key, true);\n Navigator')));
});
test('unsupported server picker is not wired into app state', () {
final mainSource = File('lib/main.dart').readAsStringSync();
final statusBar = File('lib/components/status_bar.dart').readAsStringSync();
expect(mainSource, isNot(contains('ServerList')));
expect(statusBar, isNot(contains('SelectionScreen')));
});
test('session timer rejects stale session work by generation', () {
final source = File('lib/logic/session_timer.dart').readAsStringSync();
expect(source, contains('_sessionEpoch'));
expect(source, contains('Stopwatch'));
expect(source, contains("_doDisconnect('VPN entered error state')"));
expect(source, isNot(contains('_stopForVpnFailure')));
});
}