обновил лаунчер под новую версию веба

This commit is contained in:
2026-10-04 13:19:12 +04:00
parent dd189fe48f
commit d3d19bfaeb
20 changed files with 486 additions and 64 deletions
@@ -13,14 +13,17 @@ void main() {
late GameInstallationService service;
setUp(() async {
service = GameInstallationService();
service = _RecordingGameInstallationService();
rootDirectory = await Directory.systemTemp.createTemp(
'moonwell_launcher_',
);
await File(
'${rootDirectory.path}${Platform.pathSeparator}Wow.exe',
).writeAsString('wow');
service.getExecutablePath(rootDirectory.path),
).writeAsString('moonwell');
await File(
'${rootDirectory.path}${Platform.pathSeparator}realmlist.wtf',
).writeAsString('set realmlist 127.0.0.1:3724\n');
await Directory(
'${rootDirectory.path}${Platform.pathSeparator}Data',
).create(recursive: true);
@@ -167,18 +170,196 @@ void main() {
);
expect(recordingService.arguments, isEmpty);
expect(
recordingService.executablePath,
service.getExecutablePath(rootDirectory.path),
);
expect(recordingService.workingDirectory, rootDirectory.path);
expect(recordingService.environment, <String, String>{
'MOONWELL_LAUNCH_ACCOUNT': 'PLAYERONE',
'MOONWELL_LAUNCH_TICKET': 'A1B2C3D4E5F6G7H8',
'WOW_USER': 'PLAYERONE',
'WOW_PASS': 'A1B2C3D4E5F6G7H8',
'WOW_HOST': '127.0.0.1:3724',
'WOW_DATA': '${rootDirectory.path}${Platform.pathSeparator}Data',
'BENILLA_HOME':
'${rootDirectory.path}${Platform.pathSeparator}benilla-config',
});
expect(
await File(
'${rootDirectory.path}${Platform.pathSeparator}realmlist.wtf',
).readAsString(),
'set realmlist 127.0.0.1:3724\n',
);
expect(
await File(
'${rootDirectory.path}${Platform.pathSeparator}Cache${Platform.pathSeparator}cache.bin',
).readAsString(),
'cache',
);
},
);
test(
'macOS starts the app launcher and keeps state outside the bundle',
() async {
final recordingService = _RecordingGameInstallationService(
platform: 'macos',
);
final executable = File(
recordingService.getExecutablePath(rootDirectory.path),
);
await executable.parent.create(recursive: true);
await executable.writeAsString('#!/bin/sh\n');
await recordingService.launchGame(
rootDirectory.path,
authorization: _validTicket(),
);
expect(recordingService.executablePath, executable.path);
expect(
recordingService.environment?['BENILLA_HOME'],
'${rootDirectory.path}${Platform.pathSeparator}benilla-config',
);
expect(recordingService.arguments, isEmpty);
},
);
test('uses WoW/Data when the install has a nested Data directory', () async {
await Directory(
'${rootDirectory.path}${Platform.pathSeparator}Data',
).rename('${rootDirectory.path}${Platform.pathSeparator}WoW');
final nestedData = Directory(
'${rootDirectory.path}${Platform.pathSeparator}WoW${Platform.pathSeparator}Data',
);
await nestedData.create();
final recordingService = _RecordingGameInstallationService();
await recordingService.launchGame(
rootDirectory.path,
authorization: _validTicket(),
);
expect(recordingService.environment?['WOW_DATA'], nestedData.path);
});
test('configured auth address overrides the install realmlist', () async {
final configured = _RecordingGameInstallationService(
authAddress: 'login.example:3725',
);
expect(
await configured.resolveAuthAddress(rootDirectory.path),
'login.example:3725',
);
});
test('reads a quoted realmlist with CRLF without modifying it', () async {
final file = File(
'${rootDirectory.path}${Platform.pathSeparator}realmlist.wtf',
);
await file.writeAsString('SET REALMLIST "login.example:3724"\r\n');
expect(
await service.resolveAuthAddress(rootDirectory.path),
'login.example:3724',
);
expect(
await file.readAsString(),
'SET REALMLIST "login.example:3724"\r\n',
);
});
test('rejects missing or malformed auth endpoints', () async {
final file = File(
'${rootDirectory.path}${Platform.pathSeparator}realmlist.wtf',
);
await file.delete();
await expectLater(
service.resolveAuthAddress(rootDirectory.path),
throwsA(isA<LauncherSyncException>()),
);
for (final address in [
'https://login.example',
'login.example:0',
'login.example:65536',
]) {
final configured = _RecordingGameInstallationService(
authAddress: address,
);
await expectLater(
configured.resolveAuthAddress(rootDirectory.path),
throwsA(isA<LauncherSyncException>()),
);
}
});
test('does not start a process with an expired ticket', () async {
final recordingService = _RecordingGameInstallationService();
await expectLater(
recordingService.launchGame(
rootDirectory.path,
authorization: LauncherGameTicket(
account: 'PLAYERONE',
ticket: 'A1B2C3D4E5F6G7H8',
expiresAt: DateTime.utc(2020),
),
),
throwsA(isA<LauncherSyncException>()),
);
expect(recordingService.environment, isNull);
});
test(
'does not launch a missing client or an installation without data',
() async {
final recordingService = _RecordingGameInstallationService();
final executable = File(
recordingService.getExecutablePath(rootDirectory.path),
);
await executable.delete();
await expectLater(
recordingService.launchGame(
rootDirectory.path,
authorization: _validTicket(),
),
throwsA(isA<LauncherSyncException>()),
);
await executable.writeAsString('moonwell');
await Directory(
'${rootDirectory.path}${Platform.pathSeparator}Data',
).delete(recursive: true);
await expectLater(
recordingService.launchGame(
rootDirectory.path,
authorization: _validTicket(),
),
throwsA(isA<LauncherSyncException>()),
);
expect(recordingService.environment, isNull);
},
);
});
}
LauncherGameTicket _validTicket() => LauncherGameTicket(
account: 'PLAYERONE',
ticket: 'A1B2C3D4E5F6G7H8',
expiresAt: DateTime.now().toUtc().add(const Duration(seconds: 60)),
);
final class _RecordingGameInstallationService extends GameInstallationService {
_RecordingGameInstallationService({
this.platform = 'windows',
this.authAddress = '',
});
final String platform;
final String authAddress;
@override
String get operatingSystem => platform;
@override
String get configuredAuthAddress => authAddress;
Map<String, String>? environment;
List<String>? arguments;
String? executablePath;
String? workingDirectory;
@override
Future<GameProcessHandle> startGameProcess({
@@ -189,6 +370,8 @@ final class _RecordingGameInstallationService extends GameInstallationService {
}) async {
this.arguments = List<String>.from(arguments);
this.environment = Map<String, String>.from(environment);
this.executablePath = executablePath;
this.workingDirectory = workingDirectory;
return GameProcessHandle(pid: 42, exitCode: Future<int>.value(0));
}
}
@@ -0,0 +1,48 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.dart';
void main() {
test('requests a Vanilla game ticket with bearer authentication', () async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
addTearDown(() => server.close(force: true));
final client = _LocalLauncherApiClient(server.port);
final ticketFuture = client.issueGameTicket('test-launcher-token');
final request = await server.first;
expect(request.method, 'POST');
expect(request.uri.path, '/api/launcher/game-ticket');
expect(
request.headers.value(HttpHeaders.authorizationHeader),
'Bearer test-launcher-token',
);
expect(jsonDecode(await utf8.decoder.bind(request).join()), {
'client_build': 5875,
});
request.response.headers.contentType = ContentType.json;
request.response.write(
jsonEncode({
'account': 'PLAYERONE',
'ticket': 'A1B2C3D4E5F6G7H8',
'expires_at': DateTime.now()
.toUtc()
.add(const Duration(seconds: 60))
.toIso8601String(),
}),
);
await request.response.close();
final ticket = await ticketFuture;
expect(ticket.account, 'PLAYERONE');
expect(ticket.ticket, 'A1B2C3D4E5F6G7H8');
});
}
class _LocalLauncherApiClient extends LauncherApiClient {
_LocalLauncherApiClient(this.port);
final int port;
@override
String get apiBaseUrl => 'http://127.0.0.1:$port';
}