Update Qlyra application
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s

This commit is contained in:
sevenhill
2026-08-30 22:53:15 +03:00
parent 116d3fb1d3
commit 50816588e4
4812 changed files with 14559 additions and 403 deletions
+73
View File
@@ -0,0 +1,73 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
void main() {
final manifest = File('android/app/src/main/AndroidManifest.xml');
final matrix = File('tool/android_permissions.json');
test('every Android manifest permission is present in the policy matrix', () {
final declared = RegExp(r'<uses-permission\s+android:name="([^"]+)"')
.allMatches(manifest.readAsStringSync())
.map((match) => match.group(1)!)
.toSet();
final entries = (jsonDecode(matrix.readAsStringSync()) as List)
.cast<Map<String, dynamic>>();
final documented = entries
.map((entry) => entry['permission'] as String)
.toSet();
expect(entries.length, documented.length);
expect(documented, declared);
for (final entry in entries) {
expect((entry['owner'] as String).trim(), isNotEmpty);
expect((entry['feature'] as String).trim(), isNotEmpty);
expect((entry['trigger'] as String).trim(), isNotEmpty);
}
});
test('runtime-sensitive permissions require a user action', () {
final entries = (jsonDecode(matrix.readAsStringSync()) as List)
.cast<Map<String, dynamic>>();
final byPermission = {
for (final entry in entries) entry['permission'] as String: entry,
};
final sensitive = {
'android.permission.REQUEST_INSTALL_PACKAGES',
'android.permission.CAMERA',
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.ACCESS_COARSE_LOCATION',
'android.permission.RECORD_AUDIO',
'android.permission.BLUETOOTH_CONNECT',
'android.permission.BLUETOOTH_ADVERTISE',
'android.permission.BLUETOOTH_SCAN',
'android.permission.POST_NOTIFICATIONS',
'android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS',
'android.permission.USE_FULL_SCREEN_INTENT',
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.READ_MEDIA_IMAGES',
'android.permission.READ_MEDIA_VIDEO',
'android.permission.READ_MEDIA_VISUAL_USER_SELECTED',
'android.permission.READ_CONTACTS',
};
for (final permission in sensitive) {
expect(byPermission[permission]?['trigger'], 'user_action');
}
});
test('startup paths do not request notification or full-screen access', () {
final pushSource = File(
'lib/core/push/push_service.dart',
).readAsStringSync();
final mainSource = File('lib/main.dart').readAsStringSync();
final initBody = RegExp(
r'Future<void> init\([\s\S]*?Future<bool> requestPermissionFromUser',
).firstMatch(pushSource)?.group(0);
expect(initBody, isNotNull);
expect(initBody, isNot(contains('requestPermission()')));
expect(mainSource, isNot(contains('_ensureFullScreenIntentPermission')));
});
}
+49
View File
@@ -0,0 +1,49 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
void main() {
const qualityWorkflows = [
'.github/workflows/flutter-dev.yml',
'.github/workflows/flutter-main.yml',
'.github/workflows/build-android.yml',
'.github/workflows/build-android-fcm.yml',
];
test('build workflows run formatting, analysis and tests', () {
for (final path in qualityWorkflows) {
final workflow = File(path).readAsStringSync();
expect(
workflow,
contains('dart format --output=none --set-exit-if-changed lib test'),
);
expect(workflow, contains('flutter analyze --no-fatal-infos lib test'));
expect(workflow, contains('flutter test'));
}
});
test('pull request CI produces coverage', () {
for (final path in [
'.github/workflows/flutter-dev.yml',
'.github/workflows/flutter-main.yml',
]) {
final workflow = File(path).readAsStringSync();
expect(workflow, contains('flutter test --coverage'));
expect(workflow, contains('coverage/lcov.info'));
}
});
test('dependency and Android smoke workflows stay enabled', () {
final security = File(
'.github/workflows/osv-scanner.yml',
).readAsStringSync();
final smoke = File(
'.github/workflows/android-smoke.yml',
).readAsStringSync();
expect(security, contains('osv-scanner-reusable.yml@v2.5.0'));
expect(security, contains('--lockfile=./pubspec.lock'));
expect(smoke, contains('android-emulator-runner@v2'));
expect(smoke, contains('adb shell pidof ru.qlyra.app'));
});
}
+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:qlyra/core/utils/ip_lookup_service.dart';
import 'package:qlyra/frontend/screens/profile/devices_screen.dart';
void main() {
test('devices screen can be constructed with the protected lookup flow', () {
expect(const DevicesScreen(), isA<DevicesScreen>());
});
test('normalizes an ipwho.is response', () {
final details = IpLookupDetails.fromIpWhoIs({
'success': true,
'city': 'Moscow',
'country': 'Russia',
'connection': {'asn': 64500, 'org': 'Example Org', 'isp': 'Example ISP'},
'security': {'mobile': true, 'proxy': false},
'timezone': {'id': 'Europe/Moscow'},
});
expect(details.city, 'Moscow');
expect(details.country, 'Russia');
expect(details.isp, 'Example ISP');
expect(details.network, 'AS64500 Example Org');
expect(details.mobile, isTrue);
expect(details.proxy, isFalse);
expect(details.timezone, 'Europe/Moscow');
});
test('rejects a failed provider response', () {
expect(
() => IpLookupDetails.fromIpWhoIs({
'success': false,
'message': 'Invalid IP',
}),
throwsFormatException,
);
});
}
+117
View File
@@ -0,0 +1,117 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:qlyra/core/utils/update_checker.dart';
import 'package:qlyra/core/utils/update_installer.dart';
void main() {
const qlyra = AppUpdateArtifact(
name: 'qlyra.apk',
url: 'https://example.test/qlyra.apk',
flavor: 'qlyra',
abi: 'universal',
sizeBytes: 100,
sha256: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
);
const onemeArm64 = AppUpdateArtifact(
name: 'oneme-arm64.apk',
url: 'https://example.test/oneme-arm64.apk',
flavor: 'oneme',
abi: 'arm64-v8a',
sizeBytes: 100,
sha256: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
);
const onemeUniversal = AppUpdateArtifact(
name: 'oneme.apk',
url: 'https://example.test/oneme.apk',
flavor: 'oneme',
abi: 'universal',
sizeBytes: 100,
sha256: 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
);
test('maps package identities to independent Argus slugs', () {
expect(UpdateChecker.manifestSlugForPackageName('ru.qlyra.app'), 'qlyra');
expect(UpdateChecker.manifestSlugForPackageName('ru.oneme.app'), 'oneme');
});
test('selects only an artifact for the installed flavor', () {
expect(
UpdateInstaller.selectArtifact(
[qlyra, onemeArm64, onemeUniversal],
'ru.qlyra.app',
['arm64-v8a'],
),
same(qlyra),
);
expect(
UpdateInstaller.selectArtifact(
[qlyra, onemeArm64, onemeUniversal],
'ru.oneme.app',
['arm64-v8a'],
),
same(onemeArm64),
);
});
test('falls back to a universal artifact of the same flavor', () {
expect(
UpdateInstaller.selectArtifact(
[qlyra, onemeUniversal],
'ru.oneme.app',
['x86_64'],
),
same(onemeUniversal),
);
});
test('does not cross-install another flavor', () {
expect(
UpdateInstaller.selectArtifact([qlyra], 'ru.oneme.app', ['arm64-v8a']),
isNull,
);
});
test('verifies artifact size and SHA-256', () async {
final directory = await Directory.systemTemp.createTemp('qlyra-update-');
addTearDown(() => directory.delete(recursive: true));
final file = File('${directory.path}/update.apk');
final bytes = [1, 2, 3, 4];
await file.writeAsBytes(bytes);
final artifact = AppUpdateArtifact(
name: 'update.apk',
url: 'https://example.test/update.apk',
flavor: 'qlyra',
abi: 'universal',
sizeBytes: bytes.length,
sha256: sha256.convert(bytes).toString(),
);
await expectLater(
UpdateInstaller.verifyArtifact(file, artifact),
completes,
);
});
test('rejects an artifact with a different digest', () async {
final directory = await Directory.systemTemp.createTemp('qlyra-update-');
addTearDown(() => directory.delete(recursive: true));
final file = File('${directory.path}/update.apk');
await file.writeAsBytes([1, 2, 3, 4]);
const artifact = AppUpdateArtifact(
name: 'update.apk',
url: 'https://example.test/update.apk',
flavor: 'qlyra',
abi: 'universal',
sizeBytes: 4,
sha256:
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
);
await expectLater(
UpdateInstaller.verifyArtifact(file, artifact),
throwsA(isA<UpdateIntegrityException>()),
);
});
}
+69
View File
@@ -0,0 +1,69 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:qlyra/frontend/screens/webapp/web_app_security_policy.dart';
void main() {
group('WebAppSecurityPolicy', () {
test('requires an HTTPS launch URL', () {
expect(
() => WebAppSecurityPolicy.fromLaunchUrl('http://mini.invalid/app'),
throwsFormatException,
);
expect(
() => WebAppSecurityPolicy.fromLaunchUrl('https://mini.invalid/app'),
returnsNormally,
);
});
test('allows only explicitly listed top-level origins', () {
final policy = WebAppSecurityPolicy.fromLaunchUrl(
'https://mini.invalid/start',
additionalOrigins: const ['https://auth.invalid/login'],
);
expect(
policy.allowsNavigation(Uri.parse('https://mini.invalid/next')),
isTrue,
);
expect(
policy.allowsNavigation(Uri.parse('https://auth.invalid/return')),
isTrue,
);
expect(
policy.allowsNavigation(Uri.parse('https://other.invalid/')),
isFalse,
);
expect(
policy.allowsNavigation(Uri.parse('http://mini.invalid/')),
isFalse,
);
});
test('denies permission requests from unlisted or inactive origins', () {
final policy = WebAppSecurityPolicy.fromLaunchUrl(
'https://mini.invalid/app',
);
expect(
policy.allowsPermission(
Uri.parse('https://mini.invalid'),
Uri.parse('https://mini.invalid/page'),
),
isTrue,
);
expect(
policy.allowsPermission(
Uri.parse('https://other.invalid'),
Uri.parse('https://mini.invalid/page'),
),
isFalse,
);
expect(
policy.allowsPermission(
Uri.parse('https://mini.invalid'),
Uri.parse('https://other.invalid/page'),
),
isFalse,
);
});
});
}
+10
View File
@@ -91,6 +91,16 @@ void main() {
'{"requestId":"r4","text":"hi"}',
false,
);
for (final method in [
'WebAppRequestPhone',
'WebAppDownloadFile',
'WebAppOpenCodeReader',
'WebAppBiometryRequestAccess',
'WebAppBiometryRequestAuth',
'WebAppBiometryUpdateToken',
]) {
await bridge.handleEvent(method, '{"requestId":"blocked"}', false);
}
expect(sent, isEmpty);
bridge.registerGesture();
+1 -8
View File
@@ -18,14 +18,7 @@ void main() {
});
test('hands app schemes over to the app', () {
for (final scheme in [
'max',
'MAX',
'qlyra',
'tel',
'mailto',
'intent',
]) {
for (final scheme in ['max', 'MAX', 'qlyra', 'tel', 'mailto', 'intent']) {
expect(leavesWebView(scheme), isTrue, reason: scheme);
}
});