mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:04:07 +00:00
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
208 lines
6.6 KiB
Dart
208 lines
6.6 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:drift/native.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:twonly/core/bridge.dart' as bridge;
|
|
import 'package:twonly/core/bridge/wrapper/backup.dart';
|
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
|
import 'package:twonly/core/frb_generated.dart';
|
|
import 'package:twonly/globals.dart';
|
|
import 'package:twonly/locator.dart';
|
|
import 'package:twonly/src/callbacks/callbacks.dart';
|
|
import 'package:twonly/src/database/twonly.db.dart';
|
|
import 'package:twonly/src/model/json/backup.model.dart';
|
|
import 'package:twonly/src/services/api/api.service.dart';
|
|
import 'package:twonly/src/services/backup.service.dart';
|
|
import 'package:twonly/src/services/user.service.dart';
|
|
import 'package:twonly/src/utils/keyvalue.dart';
|
|
|
|
import '../mocks/platform_channels.dart';
|
|
import '../mocks/user_config.dart';
|
|
|
|
void main() {
|
|
if (!Platform.isMacOS) {
|
|
return;
|
|
}
|
|
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late Directory tempDir;
|
|
late Map<String, dynamic> initialUserData;
|
|
|
|
setUpAll(() async {
|
|
const pathProviderChannel = MethodChannel(
|
|
'plugins.flutter.io/path_provider',
|
|
);
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(pathProviderChannel, (methodCall) async {
|
|
if (methodCall.method == 'getApplicationSupportDirectory') {
|
|
return tempDir.path;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
final dylibPath =
|
|
'${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib';
|
|
if (File(dylibPath).existsSync()) {
|
|
await RustLib.init(
|
|
externalLibrary: ExternalLibrary.open(dylibPath),
|
|
);
|
|
} else {
|
|
await RustLib.init();
|
|
}
|
|
await initFlutterCallbacksForRust();
|
|
|
|
tempDir = Directory.systemTemp.createTempSync('twonly_backup_test_');
|
|
AppEnvironment.initTesting(
|
|
customCacheDir: tempDir.path,
|
|
customSupportDir: tempDir.path,
|
|
);
|
|
|
|
await bridge.initializeTwonlyFlutter(
|
|
config: bridge.InitConfig(
|
|
databaseDir: tempDir.path,
|
|
dataDir: tempDir.path,
|
|
),
|
|
);
|
|
});
|
|
|
|
setUp(() async {
|
|
setupPlatformChannelMocks();
|
|
await locator.reset();
|
|
final dbFile = File('${tempDir.path}/twonly.sqlite');
|
|
locator
|
|
..registerSingleton<TwonlyDB>(
|
|
TwonlyDB(NativeDatabase(dbFile)),
|
|
)
|
|
..registerSingleton<UserService>(UserService())
|
|
..registerSingleton<ApiService>(ApiService());
|
|
|
|
userService.currentUser = testUserConfig(
|
|
userId: 1,
|
|
username: 'test_user',
|
|
displayName: 'Test User',
|
|
subscriptionPlan: 'Free',
|
|
currentSetupPage: null,
|
|
appVersion: 100,
|
|
);
|
|
userService.isUserCreated = true;
|
|
await UserService.save(userService.currentUser);
|
|
initialUserData = (await KeyValueStore.get('user'))!;
|
|
|
|
await RustBackupIdentity.setBackupPasswordKeys(
|
|
password: 'strong_password',
|
|
userId: 1,
|
|
);
|
|
});
|
|
|
|
tearDown(() async {
|
|
try {
|
|
await twonlyDB.close();
|
|
} catch (_) {}
|
|
});
|
|
|
|
tearDownAll(() async {
|
|
if (tempDir.existsSync()) {
|
|
try {
|
|
tempDir.deleteSync(recursive: true);
|
|
} catch (_) {}
|
|
}
|
|
});
|
|
|
|
group('BackupService Tests', () {
|
|
test('getData returns default backup status initially', () async {
|
|
final data = await BackupService.getData();
|
|
expect(data.identityState, LastBackupUploadState.none);
|
|
expect(data.archiveState, LastBackupUploadState.none);
|
|
expect(data.identityLastSuccessFull, isNull);
|
|
expect(data.archiveLastSuccessFull, isNull);
|
|
});
|
|
|
|
test(
|
|
'startFullBackupRecovery returns usernameNotValid for offline/unknown user',
|
|
() async {
|
|
final error = await BackupService.startFullBackupRecovery(
|
|
'unknown_user',
|
|
'password',
|
|
);
|
|
expect(error, RecoveryError.usernameNotValid);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'Full backup recovery flow restores identity and user.json archive successfully',
|
|
() async {
|
|
final initialBackupIdStr = await RustBackupIdentity.getBackupId();
|
|
|
|
// 1. Create backups of baseline state purely natively to avoid background backup races
|
|
final identityBytes = await RustBackupIdentity.getIdentityBackupBytes();
|
|
final (_, archivePath) = await RustBackupArchive.createBackupArchive();
|
|
|
|
// 2. Tamper with user.json data and verify alteration
|
|
await KeyValueStore.put(
|
|
'user',
|
|
{'changed': true, 'username': 'tampered'},
|
|
);
|
|
|
|
final changedUserData = await KeyValueStore.get('user');
|
|
expect(changedUserData?['changed'], isTrue);
|
|
|
|
// 3. Trigger a change of the key_manager before restoring
|
|
await RustBackupIdentity.importBackupPasswordKeys(
|
|
backupId: List.filled(32, 1),
|
|
encryptionKey: List.filled(32, 1),
|
|
);
|
|
|
|
final changedBackupIdStr = await RustBackupIdentity.getBackupId();
|
|
expect(changedBackupIdStr, isNot(equals(initialBackupIdStr)));
|
|
|
|
// 4. Restore identity and archive
|
|
final backupKeys = await RustBackupIdentity.getBackupPasswordKeys(
|
|
userId: 1,
|
|
password: 'strong_password',
|
|
);
|
|
await RustBackupIdentity.restoreIdentityBackup(
|
|
keys: backupKeys,
|
|
encryptedBytes: identityBytes,
|
|
);
|
|
|
|
await RustBackupArchive.restoreBackupArchive(filePath: archivePath);
|
|
|
|
final restoredBackupIdStr = await RustBackupIdentity.getBackupId();
|
|
expect(restoredBackupIdStr, equals(initialBackupIdStr));
|
|
|
|
// 5. Verify user.json data is fully restored
|
|
final restoredUserData = await KeyValueStore.get('user');
|
|
expect(
|
|
restoredUserData?['username'],
|
|
equals(initialUserData['username']),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'startPasswordlessBackupRecovery restores identity and returns tryAgainLater on download',
|
|
() async {
|
|
final keyManagerBytes = await RustKeyManager.serialize();
|
|
|
|
final error = await BackupService.startPasswordlessBackupRecovery(
|
|
1,
|
|
'test_user',
|
|
keyManagerBytes,
|
|
);
|
|
expect(error, RecoveryError.tryAgainLater);
|
|
|
|
// Verify key manager was imported successfully
|
|
final recoveredUserId = await RustKeyManager.getUserId();
|
|
expect(recoveredUserId, 1);
|
|
|
|
// Restore mock DB to prevent tearDown crash (deleteLocalUserData overrides it)
|
|
locator.unregister<TwonlyDB>();
|
|
final dbFile = File('${tempDir.path}/twonly.sqlite');
|
|
locator.registerSingleton<TwonlyDB>(TwonlyDB(NativeDatabase(dbFile)));
|
|
},
|
|
);
|
|
});
|
|
}
|