обновленная синхронизация клиента
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
// import 'package:moonwell_launcher/config.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/data/game_installation_service.dart';
|
||||
// import 'package:moonwell_launcher/features/launcher/domain/entities/client_hash_entry.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_manifest.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_exception.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_game_ticket.dart';
|
||||
|
||||
@@ -52,37 +52,278 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
// test('scanInstallation ignores excluded directories', () async {
|
||||
// final snapshot = await service.scanInstallation(rootDirectory.path);
|
||||
// final paths = snapshot.files.map((file) => file.path).toList()..sort();
|
||||
test(
|
||||
'finds only unmanaged Data files and legacy client binaries',
|
||||
() async {
|
||||
final retainedPaths = [
|
||||
'MoonWell.exe',
|
||||
'Data/common.MPQ',
|
||||
'Data/ruRU/realmlist.wtf',
|
||||
'Data/Addons/required.MPQ',
|
||||
'required.DLL',
|
||||
'WoW/required.dll',
|
||||
'WoW/Data/common.MPQ',
|
||||
];
|
||||
final staleFiles = [
|
||||
'Data/old.MPQ',
|
||||
'Data/ruRU/old.MPQ',
|
||||
'Data/Addons/extra.MPQ',
|
||||
'Data/Cache/old.bin',
|
||||
'Data/empty/old.MPQ',
|
||||
'Data/common.MPQ.moonwell.part',
|
||||
'Data/broken.MPQ.moonwell.part.failed',
|
||||
'WoW.exe',
|
||||
'old.DLL',
|
||||
'WoW/WoW.exe',
|
||||
'WoW/old.dll',
|
||||
'WoW/Data/old.MPQ',
|
||||
];
|
||||
final untouchedPaths = [
|
||||
'Cache/cache.bin',
|
||||
'Logs/launcher.log',
|
||||
'WTF/Config.wtf',
|
||||
'Errors/error.dll',
|
||||
'Screenshots/shot.dll',
|
||||
'benilla-config/settings.dll',
|
||||
'.moonwell_launcher/local.dll',
|
||||
'Interface/AddOns/custom/addon.dll',
|
||||
'notes.txt',
|
||||
'Other.exe',
|
||||
'realmlist.wtf',
|
||||
];
|
||||
for (final path in [
|
||||
...retainedPaths,
|
||||
...staleFiles,
|
||||
...untouchedPaths,
|
||||
]) {
|
||||
final file = File(
|
||||
service.resolveClientPath(rootDirectory.path, path),
|
||||
);
|
||||
await file.parent.create(recursive: true);
|
||||
await file.writeAsString(path);
|
||||
}
|
||||
await Directory(
|
||||
service.resolveClientPath(rootDirectory.path, 'Data/unused/nested'),
|
||||
).create(recursive: true);
|
||||
final manifest = await _manifestForPaths(
|
||||
service,
|
||||
rootDirectory,
|
||||
retainedPaths,
|
||||
);
|
||||
|
||||
// expect(paths, ['Data/common.MPQ', 'Wow.exe']);
|
||||
// expect(snapshot.buildHash, computeBuildHash(snapshot.files));
|
||||
// });
|
||||
final snapshot = await service.scanInstallation(
|
||||
rootDirectory.path,
|
||||
manifest: manifest,
|
||||
);
|
||||
|
||||
// test(
|
||||
// 'scanInstallation creates launcher hash cache outside verification set',
|
||||
// () async {
|
||||
// await service.scanInstallation(rootDirectory.path);
|
||||
expect(
|
||||
snapshot.files.map((file) => file.path),
|
||||
unorderedEquals(retainedPaths),
|
||||
);
|
||||
expect(snapshot.buildHash, manifest.buildHash);
|
||||
expect(
|
||||
snapshot.stalePaths,
|
||||
unorderedEquals([
|
||||
...staleFiles,
|
||||
'Data/Cache',
|
||||
'Data/empty',
|
||||
'Data/unused/nested',
|
||||
'Data/unused',
|
||||
]),
|
||||
);
|
||||
for (final path in snapshot.stalePaths) {
|
||||
await service.deleteRelativeFile(rootDirectory.path, path);
|
||||
}
|
||||
for (final path in [...retainedPaths, ...untouchedPaths]) {
|
||||
expect(
|
||||
await File(
|
||||
service.resolveClientPath(rootDirectory.path, path),
|
||||
).exists(),
|
||||
isTrue,
|
||||
reason: path,
|
||||
);
|
||||
}
|
||||
for (final path in snapshot.stalePaths) {
|
||||
expect(
|
||||
await FileSystemEntity.type(
|
||||
service.resolveClientPath(rootDirectory.path, path),
|
||||
followLinks: false,
|
||||
),
|
||||
FileSystemEntityType.notFound,
|
||||
reason: path,
|
||||
);
|
||||
}
|
||||
|
||||
// final cacheFile = File(service.getHashCachePath(rootDirectory.path));
|
||||
// expect(await cacheFile.exists(), isTrue);
|
||||
final verified = await service.scanInstallation(
|
||||
rootDirectory.path,
|
||||
manifest: manifest,
|
||||
);
|
||||
expect(verified.stalePaths, isEmpty);
|
||||
expect(verified.buildHash, manifest.buildHash);
|
||||
expect(
|
||||
await File(service.getHashCachePath(rootDirectory.path)).exists(),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// final secondSnapshot = await service.scanInstallation(
|
||||
// rootDirectory.path,
|
||||
// );
|
||||
// final paths = secondSnapshot.files.map((file) => file.path).toList()
|
||||
// ..sort();
|
||||
test('removes Data links without touching their targets', () async {
|
||||
final outside = await Directory.systemTemp.createTemp(
|
||||
'moonwell_outside_',
|
||||
);
|
||||
addTearDown(() => outside.delete(recursive: true));
|
||||
final outsideFile = File(
|
||||
'${outside.path}${Platform.pathSeparator}keep.dll',
|
||||
);
|
||||
await outsideFile.writeAsString('keep');
|
||||
final link = Link(
|
||||
service.resolveClientPath(rootDirectory.path, 'Data/external'),
|
||||
);
|
||||
await link.create(outside.path);
|
||||
final manifest = await _manifestForPaths(service, rootDirectory, [
|
||||
'Data/common.MPQ',
|
||||
]);
|
||||
|
||||
// expect(paths, ['Data/common.MPQ', 'Wow.exe']);
|
||||
// expect(
|
||||
// await Directory(
|
||||
// '${rootDirectory.path}${Platform.pathSeparator}${Config.launcherMetadataDirectoryName}',
|
||||
// ).exists(),
|
||||
// isTrue,
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
final snapshot = await service.scanInstallation(
|
||||
rootDirectory.path,
|
||||
manifest: manifest,
|
||||
);
|
||||
expect(snapshot.stalePaths, ['Data/external']);
|
||||
await service.deleteRelativeFile(rootDirectory.path, 'Data/external');
|
||||
expect(await link.exists(), isFalse);
|
||||
expect(await outsideFile.readAsString(), 'keep');
|
||||
});
|
||||
|
||||
test('retains a directory link needed by manifest files', () async {
|
||||
final target = Directory(
|
||||
service.resolveClientPath(rootDirectory.path, 'shared-data'),
|
||||
);
|
||||
await target.create();
|
||||
await File(
|
||||
'${target.path}${Platform.pathSeparator}required.MPQ',
|
||||
).writeAsString('required');
|
||||
final link = Link(
|
||||
service.resolveClientPath(rootDirectory.path, 'Data/linked'),
|
||||
);
|
||||
await link.create(target.path);
|
||||
final manifest = await _manifestForPaths(service, rootDirectory, [
|
||||
'Data/common.MPQ',
|
||||
'Data/linked/required.MPQ',
|
||||
]);
|
||||
final snapshot = await service.scanInstallation(
|
||||
rootDirectory.path,
|
||||
manifest: manifest,
|
||||
);
|
||||
expect(snapshot.stalePaths, isEmpty);
|
||||
expect(snapshot.buildHash, manifest.buildHash);
|
||||
expect(await link.exists(), isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'rejects deleting through a directory link outside installation',
|
||||
() async {
|
||||
final outside = await Directory.systemTemp.createTemp(
|
||||
'moonwell_outside_',
|
||||
);
|
||||
addTearDown(() => outside.delete(recursive: true));
|
||||
final outsideFile = File(
|
||||
'${outside.path}${Platform.pathSeparator}keep.dll',
|
||||
);
|
||||
await outsideFile.writeAsString('keep');
|
||||
final link = Link(
|
||||
service.resolveClientPath(rootDirectory.path, 'Data/external'),
|
||||
);
|
||||
await link.create(outside.path);
|
||||
await expectLater(
|
||||
service.deleteRelativeFile(
|
||||
rootDirectory.path,
|
||||
'Data/external/keep.dll',
|
||||
),
|
||||
throwsA(isA<LauncherSyncException>()),
|
||||
);
|
||||
expect(await outsideFile.readAsString(), 'keep');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'preserves Windows manifest files despite path case differences',
|
||||
() async {
|
||||
final file = File(
|
||||
service.resolveClientPath(
|
||||
rootDirectory.path,
|
||||
'Data/ruRU/required.DLL',
|
||||
),
|
||||
);
|
||||
await file.parent.create(recursive: true);
|
||||
await file.writeAsString('required');
|
||||
final manifest = await _manifestForPaths(service, rootDirectory, [
|
||||
'data/ruru/required.dll',
|
||||
]);
|
||||
final snapshot = await service.scanInstallation(
|
||||
rootDirectory.path,
|
||||
manifest: manifest,
|
||||
);
|
||||
expect(snapshot.stalePaths, ['Data/common.MPQ']);
|
||||
expect(snapshot.buildHash, manifest.buildHash);
|
||||
},
|
||||
skip: !Platform.isWindows,
|
||||
);
|
||||
|
||||
test('preserves WoW.exe when explicitly present in manifest', () async {
|
||||
await File(
|
||||
service.resolveClientPath(rootDirectory.path, 'WoW.exe'),
|
||||
).writeAsString('legacy');
|
||||
final manifest = await _manifestForPaths(service, rootDirectory, [
|
||||
'WoW.exe',
|
||||
'Data/common.MPQ',
|
||||
]);
|
||||
final snapshot = await service.scanInstallation(
|
||||
rootDirectory.path,
|
||||
manifest: manifest,
|
||||
);
|
||||
expect(snapshot.stalePaths, isEmpty);
|
||||
expect(snapshot.buildHash, manifest.buildHash);
|
||||
});
|
||||
|
||||
test('rejects invalid manifest paths before scanning', () async {
|
||||
for (final path in [
|
||||
'../outside.dll',
|
||||
'/outside.dll',
|
||||
r'C:\outside.dll',
|
||||
'C:outside.dll',
|
||||
'Data/file:stream',
|
||||
]) {
|
||||
final manifest = ClientManifest.fromJson({
|
||||
'files': [
|
||||
{'path': path, 'size': 0, 'sha256': ''},
|
||||
],
|
||||
});
|
||||
await expectLater(
|
||||
service.scanInstallation(rootDirectory.path, manifest: manifest),
|
||||
throwsA(isA<LauncherSyncException>()),
|
||||
);
|
||||
}
|
||||
expect(
|
||||
await File(service.getHashCachePath(rootDirectory.path)).exists(),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'never recursively deletes a stale directory with new files',
|
||||
() async {
|
||||
final file = File(
|
||||
service.resolveClientPath(rootDirectory.path, 'Data/old/new.MPQ'),
|
||||
);
|
||||
await file.parent.create(recursive: true);
|
||||
await file.writeAsString('new');
|
||||
await expectLater(
|
||||
service.deleteRelativeFile(rootDirectory.path, 'Data/old'),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
expect(await file.readAsString(), 'new');
|
||||
},
|
||||
);
|
||||
|
||||
test('clearCache recreates an empty cache directory', () async {
|
||||
await service.clearCache(rootDirectory.path);
|
||||
@@ -335,6 +576,25 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
Future<ClientManifest> _manifestForPaths(
|
||||
GameInstallationService service,
|
||||
Directory rootDirectory,
|
||||
List<String> paths,
|
||||
) async {
|
||||
final files = <Map<String, Object>>[];
|
||||
for (final path in paths) {
|
||||
final bytes = await File(
|
||||
service.resolveClientPath(rootDirectory.path, path),
|
||||
).readAsBytes();
|
||||
files.add({
|
||||
'path': path,
|
||||
'size': bytes.length,
|
||||
'sha256': sha256.convert(bytes).toString(),
|
||||
});
|
||||
}
|
||||
return ClientManifest.fromJson({'files': files});
|
||||
}
|
||||
|
||||
LauncherGameTicket _validTicket() => LauncherGameTicket(
|
||||
account: 'PLAYERONE',
|
||||
ticket: 'A1B2C3D4E5F6G7H8',
|
||||
|
||||
Reference in New Issue
Block a user