@TestOn('windows') library; import 'dart:ffi'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:matrix/matrix.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqlite3/open.dart' as sqlite_open; /// Setup-Helfer für die Call-Tests mit den Autopilot-Test-Accounts (gegated /// wie `live_encryption_e2e_test.dart`, läuft NIE im normalen `flutter test`): /// stellt sicher, dass ein Voice-Channel-Raum („Pyramid Autopilot Voice", /// `io.pyramid.voice_channel` + Presence-PowerLevel 0 wie im /// create_join_dialog) existiert und BEIDE Test-Nutzer Mitglied sind. /// Danach können zwei GUI-Instanzen (PYRAMID_PROFILE_DIR) den Kanal joinen. /// /// Kein Logout am Ende nötig/gewollt für die GUI-Sessions – die Skript-Geräte /// melden sich selbst ab, die GUI-Tokens bleiben unberührt. const _kVoiceRoomName = 'Pyramid Autopilot Voice'; Map? _loadAccounts() { final profile = Platform.environment['USERPROFILE'] ?? ''; final file = File('$profile\\.pyramid-autopilot\\test-accounts.txt'); if (!file.existsSync()) return null; final map = {}; for (final line in file.readAsLinesSync()) { final t = line.trim(); if (t.isEmpty || t.startsWith('#')) continue; final i = t.indexOf('='); if (i > 0) map[t.substring(0, i)] = t.substring(i + 1); } return map; } String? _findSqliteDll() { for (final config in ['Debug', 'Profile', 'Release']) { final file = File('build/windows/x64/runner/$config/sqlite3.dll'); if (file.existsSync()) return file.absolute.path; } return null; } Future _login(String name, String user, String pass, Uri hs) async { // Wegwerf-Client: In-Memory-DB reicht (die DLL aus dem App-Build laden, // der flutter_tester bringt selbst keine sqlite3.dll mit). final dll = _findSqliteDll()!; final factory = createDatabaseFactoryFfi( ffiInit: () { sqlite_open.open.overrideFor( sqlite_open.OperatingSystem.windows, () => DynamicLibrary.open(dll), ); }, noIsolate: true, ); final db = await factory.openDatabase(inMemoryDatabasePath); final sdkDb = await MatrixSdkDatabase.init(name, database: db); final client = Client(name, database: sdkDb); await client.init(waitForFirstSync: false, waitUntilLoadCompletedLoaded: false); await client.checkHomeserver(hs); await client.login( LoginType.mLoginPassword, identifier: AuthenticationUserIdentifier(user: user), password: pass, initialDeviceDisplayName: name, ); return client; } void main() { final acc = _loadAccounts(); final skip = Platform.environment['PYRAMID_LIVE_TEST'] != '1' ? 'Live-Setup nur mit PYRAMID_LIVE_TEST=1' : acc == null ? 'Test-Account-Datei fehlt' : null; test('Voice-Channel-Testraum existiert und beide Test-Nutzer sind drin', () async { final hs = Uri.parse(acc!['homeserver']!); final c1 = await _login('AutopilotCallSetup1', acc['user1']!, acc['pass1']!, hs); final c2 = await _login('AutopilotCallSetup2', acc['user2']!, acc['pass2']!, hs); try { // ignore: avoid_print print('[setup] beide Logins ok, erster Sync…'); await c1.oneShotSync().timeout(const Duration(seconds: 60)); // ignore: avoid_print print('[setup] c1 sync ok, suche Raum…'); Room? voiceRoom; for (final r in c1.rooms) { if (r.membership == Membership.join && r.name == _kVoiceRoomName && r.getState(EventTypes.RoomCreate)?.content['type'] == 'io.pyramid.voice_channel') { voiceRoom = r; break; } } if (voiceRoom == null) { // ignore: avoid_print print('[setup] Raum fehlt, lege an…'); final roomId = await c1.createRoom( name: _kVoiceRoomName, visibility: Visibility.private, preset: CreateRoomPreset.privateChat, creationContent: {'type': 'io.pyramid.voice_channel'}, ); // Presence muss für normale Mitglieder schreibbar sein – exakt wie // in create_join_dialog.dart (Continuwuity ignoriert // power_level_content_override beim createRoom). final plRaw = await c1.getRoomStateWithKey(roomId, EventTypes.RoomPowerLevels, ''); final pl = Map.from(plRaw as Map); final events = Map.from(pl['events'] as Map? ?? {}); events['io.pyramid.voice.presence'] = 0; pl['events'] = events; await c1.setRoomStateWithKey(roomId, EventTypes.RoomPowerLevels, '', pl); // ignore: avoid_print print('[setup] Raum $roomId angelegt, warte auf Sync…'); await c1.waitForRoomInSync(roomId, join: true) .timeout(const Duration(seconds: 60)); voiceRoom = c1.getRoomById(roomId); } expect(voiceRoom, isNotNull); // ignore: avoid_print print('[setup] Raum da: ${voiceRoom!.id}, hole user2 rein…'); // user2 einladen (falls noch nicht Mitglied) und beitreten lassen. final u2 = c2.userID!; await c2.oneShotSync().timeout(const Duration(seconds: 60)); // ignore: avoid_print print('[setup] c2 sync ok…'); final r2 = c2.getRoomById(voiceRoom.id); if (r2 == null || r2.membership != Membership.join) { if (r2 == null || r2.membership != Membership.invite) { try { await voiceRoom.invite(u2); } on MatrixException catch (e) { // "already in the room" o. ä. ist ok. if (e.error != MatrixError.M_FORBIDDEN) rethrow; } } await c2.joinRoom(voiceRoom.id); await c2.waitForRoomInSync(voiceRoom.id, join: true) .timeout(const Duration(seconds: 60)); } final members = await voiceRoom.requestParticipants(); final joined = members .where((m) => m.membership == Membership.join) .map((m) => m.id.toLowerCase()) .toSet(); expect(joined.contains(acc['user1']!.toLowerCase()), isTrue); expect(joined.contains(u2.toLowerCase()), isTrue, reason: 'user2 ist dem Voice-Raum nicht beigetreten'); // ignore: avoid_print print('Voice-Testraum bereit: ${voiceRoom.id}'); } finally { // Nur die Skript-Geräte abmelden – GUI-Sessions bleiben unberührt. try { await c1.logout(); } catch (_) {} try { await c2.logout(); } catch (_) {} await c1.dispose(closeDatabase: false); await c2.dispose(closeDatabase: false); } }, skip: skip, timeout: const Timeout(Duration(minutes: 5))); }