fix: Abmelden ohne verspaetete Sync-Antwort (SDK-Wettlauf) + Live-Regressionstest
logoutWithoutStraySync (lib/core/session_logout.dart): Sync-Schleife anhalten, laufenden Long-Poll per Account-Daten wecken und auslaufen lassen, erst dann abmelden - es ist keine Anfrage mit altem Token mehr unterwegs, die per 401 ein zweites clear() ausloesen koennte. Genutzt an allen drei Abmelde-Stellen (Einstellungen, Ueber, Konto geloescht) und im E2E-Live-Test. Live gemessen: die verspaetete 401 tritt real auf, kam aber stets vor dem Abschluss einer Neuanmeldung an - Risiko war klein, ist jetzt ausgeschlossen. test/live_logout_race_test.dart in scripts/test.ps1 -Live. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f09e4875c6
commit
e90b954348
@@ -42,6 +42,13 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
|
|||||||
v0.1.36 (SHA-256 c3de66be…), Release-APK startet auf dem Emulator ohne
|
v0.1.36 (SHA-256 c3de66be…), Release-APK startet auf dem Emulator ohne
|
||||||
Fehler. NICHT auf Gitea veröffentlicht (Bernd testet zuerst).
|
Fehler. NICHT auf Gitea veröffentlicht (Bernd testet zuerst).
|
||||||
|
|
||||||
|
- **SDK-Logout-Wettlauf** geschlossen: `lib/core/session_logout.dart`
|
||||||
|
(`logoutWithoutStraySync`) an allen drei Abmelde-Stellen; Live-Test
|
||||||
|
`test/live_logout_race_test.dart` (Leerlauf + direkt nach Login, je 45 s
|
||||||
|
beobachtet). Ehrliche Einordnung: verspätete 401 tritt real auf, kam aber
|
||||||
|
stets vor Abschluss der Neuanmeldung – Risiko war klein. E2E-Live-Test nutzt
|
||||||
|
jetzt denselben App-Baustein.
|
||||||
|
|
||||||
**Offen/Nächster Schritt:** Bernd testet 0.1.37 auf dem Handy (Push mit
|
**Offen/Nächster Schritt:** Bernd testet 0.1.37 auf dem Handy (Push mit
|
||||||
mehreren Nachrichten + Bild, „Gelesen“, Statusleiste, Vollansicht-Gesten,
|
mehreren Nachrichten + Bild, „Gelesen“, Statusleiste, Vollansicht-Gesten,
|
||||||
Sprechblasen, Schriftgröße). Danach ROADMAP: SDK-Logout-Wettlauf im
|
Sprechblasen, Schriftgröße). Danach ROADMAP: SDK-Logout-Wettlauf im
|
||||||
|
|||||||
+9
-2
@@ -169,8 +169,15 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert.
|
|||||||
E2EE-Empfang live belegt, auth_log unauffällig; App läuft seit 12:43
|
E2EE-Empfang live belegt, auth_log unauffällig; App läuft seit 12:43
|
||||||
für die Langzeit-Beobachtung weiter. **Offen (Klick/Bernd):**
|
für die Langzeit-Beobachtung weiter. **Offen (Klick/Bernd):**
|
||||||
Senden, Logout-Warnung, Re-Login mit Lesbarkeits-Check.
|
Senden, Logout-Warnung, Re-Login mit Lesbarkeits-Check.
|
||||||
- [ ] **SDK-Wettlauf: verspätete 401 einer alten Sync-Anfrage löscht ggf. die
|
- [x] **SDK-Wettlauf: verspätete 401 einer alten Sync-Anfrage löscht ggf. die
|
||||||
NEUE Session** (gefunden 2026-10-07 über einen sporadisch roten
|
NEUE Session** – ERLEDIGT 2026-10-07: alle App-Abmeldungen laufen über
|
||||||
|
`logoutWithoutStraySync` (lib/core/session_logout.dart: Sync anhalten,
|
||||||
|
Long-Poll per Account-Daten wecken, auslaufen lassen, dann abmelden).
|
||||||
|
Live gemessen (test/live_logout_race_test.dart): die verspätete 401 tritt
|
||||||
|
real auf, kam aber in allen Läufen an, BEVOR die Neuanmeldung fertig
|
||||||
|
war – reales Risiko in der App klein (Neuanmeldung binnen ~1 s nötig),
|
||||||
|
jetzt ausgeschlossen; Regressionstest in `scripts/test.ps1 -Live`.
|
||||||
|
Ursprüngliche Analyse: (gefunden 2026-10-07 über einen sporadisch roten
|
||||||
Live-Test, siehe PROGRESS.md). matrix 6.2 `Client._innerSync` wirft den
|
Live-Test, siehe PROGRESS.md). matrix 6.2 `Client._innerSync` wirft den
|
||||||
Fehler einer Sync-Antwort, BEVOR geprüft wird, ob die Anfrage noch
|
Fehler einer Sync-Antwort, BEVOR geprüft wird, ob die Anfrage noch
|
||||||
aktuell ist (`_currentSyncId`). Läuft beim Logout noch ein Long-Poll
|
aktuell ist (`_currentSyncId`). Läuft beim Logout noch ein Long-Poll
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ oder zwei echten Geräten prüfen kann, steht weiterhin in `docs/PC_TESTPLAN.md`
|
|||||||
| `streaming_presets_test.dart` | 60-FPS-Streaming-Presets (Auflösung/FPS/Bitrate) | immer |
|
| `streaming_presets_test.dart` | 60-FPS-Streaming-Presets (Auflösung/FPS/Bitrate) | immer |
|
||||||
| `local_message_search_test.dart` | Lokale Nachrichtensuche (Strg+K): Treffer, Sortierung, Groß-/Kleinschreibung inkl. Umlaute, Medien-Filter, Trefferdeckel | immer (neu 2026-10-07) |
|
| `local_message_search_test.dart` | Lokale Nachrichtensuche (Strg+K): Treffer, Sortierung, Groß-/Kleinschreibung inkl. Umlaute, Medien-Filter, Trefferdeckel | immer (neu 2026-10-07) |
|
||||||
| `live_encryption_e2e_test.dart` | Echter Login (pyramidtest1) → Platte verschlüsselt → Neustart → Logout → Re-Login → alte Nachricht lesbar; Utas Update-Fall mit echter Session | nur mit `-Live` |
|
| `live_encryption_e2e_test.dart` | Echter Login (pyramidtest1) → Platte verschlüsselt → Neustart → Logout → Re-Login → alte Nachricht lesbar; Utas Update-Fall mit echter Session | nur mit `-Live` |
|
||||||
|
| `live_logout_race_test.dart` | Abmelden + sofortige Neuanmeldung auf demselben Client: neue Session muss bleiben (SDK-Wettlauf, `logoutWithoutStraySync`); mit `PYRAMID_LIVE_RACE_PROOF=1` zusätzlich Nachweis mit altem Weg | nur mit `-Live` |
|
||||||
|
| `live_ui_fixture_test.dart` | Kein Test im engeren Sinn: legt Testinhalte (DM + Gruppe mit Bildern) für den UI-Durchlauf an | nur manuell mit `PYRAMID_LIVE_TEST=1` |
|
||||||
| `live_call_setup_test.dart` | Kein Test im engeren Sinn: legt den Voice-Testraum für GUI-Call-Tests mit zwei Test-Accounts an | nur manuell mit `PYRAMID_LIVE_TEST=1` |
|
| `live_call_setup_test.dart` | Kein Test im engeren Sinn: legt den Voice-Testraum für GUI-Call-Tests mit zwei Test-Accounts an | nur manuell mit `PYRAMID_LIVE_TEST=1` |
|
||||||
|
|
||||||
`test/support/synthetic_matrix_data.dart` erzeugt synthetische Räume und
|
`test/support/synthetic_matrix_data.dart` erzeugt synthetische Räume und
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
/// Kennung der Account-Daten, mit denen wir den laufenden Sync aufwecken.
|
||||||
|
const logoutWakeAccountDataType = 'io.pyramid.logout_wake';
|
||||||
|
|
||||||
|
/// Meldet [client] ab, ohne dass eine verspätete Server-Antwort die NÄCHSTE
|
||||||
|
/// Anmeldung wieder löscht.
|
||||||
|
///
|
||||||
|
/// Hintergrund (SDK-Wettlauf, ROADMAP M1, gefunden 2026-10-07): matrix 6.2
|
||||||
|
/// (und Upstream `main`) prüft bei einer FEHLER-Antwort des Sync-Long-Polls
|
||||||
|
/// nicht, ob die Anfrage noch aktuell ist. Läuft beim Abmelden noch ein
|
||||||
|
/// Long-Poll (bis 30 s) mit dem alten Token, kommt dessen 401
|
||||||
|
/// (M_UNKNOWN_TOKEN) später zurück und das SDK ruft `clear()` – meldet sich
|
||||||
|
/// der Nutzer in der Zwischenzeit auf demselben Client neu an (Pyramid
|
||||||
|
/// benutzt nach dem Logout dasselbe Client-Objekt weiter), wird die frische
|
||||||
|
/// Session gelöscht: ein „Random-Logout“ direkt nach dem Login.
|
||||||
|
///
|
||||||
|
/// Ablauf hier:
|
||||||
|
/// 1. Sync-Schleife anhalten (nach der laufenden Anfrage keine neue).
|
||||||
|
/// 2. Laufenden Long-Poll WECKEN: geänderte Account-Daten lässt der Server
|
||||||
|
/// sofort antworten – statt bis zu 30 s zu warten.
|
||||||
|
/// 3. Die laufende Anfrage regulär (mit gültigem Token) zu Ende gehen lassen.
|
||||||
|
/// 4. Erst dann serverseitig abmelden – es ist keine Anfrage mehr unterwegs.
|
||||||
|
///
|
||||||
|
/// Schritt 2/3 sind „best effort“: scheitern sie (offline), wird nach
|
||||||
|
/// höchstens ~8 s trotzdem abgemeldet – nie schlechter als das bisherige
|
||||||
|
/// direkte `logout()`.
|
||||||
|
///
|
||||||
|
/// Live gemessen (test/live_logout_race_test.dart, 2026-10-07): die
|
||||||
|
/// verspätete 401 tritt real auf, kommt aber meist an, BEVOR ein Mensch sich
|
||||||
|
/// neu angemeldet hat – das Risiko ist klein, dieser Weg schließt es aus.
|
||||||
|
Future<void> logoutWithoutStraySync(
|
||||||
|
Client client, {
|
||||||
|
Duration settleTimeout = const Duration(seconds: 5),
|
||||||
|
}) async {
|
||||||
|
client.backgroundSync = false;
|
||||||
|
// Liefert den laufenden Sync (oder startet einen kurzen ohne Long-Poll).
|
||||||
|
final inFlight = client.oneShotSync(timeout: Duration.zero);
|
||||||
|
final userId = client.userID;
|
||||||
|
if (userId != null) {
|
||||||
|
try {
|
||||||
|
await client
|
||||||
|
.setAccountData(userId, logoutWakeAccountDataType, {
|
||||||
|
'ts': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
})
|
||||||
|
.timeout(const Duration(seconds: 3));
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[LOGOUT] Sync-Wecken fehlgeschlagen (egal): $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await inFlight.timeout(settleTimeout);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[LOGOUT] laufender Sync nicht rechtzeitig fertig: $e');
|
||||||
|
}
|
||||||
|
await client.logout();
|
||||||
|
}
|
||||||
@@ -130,7 +130,7 @@ class _AboutSection extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
if (confirm == true) {
|
if (confirm == true) {
|
||||||
unawaited(AuthLog.write('User-initiated logout (Settings)'));
|
unawaited(AuthLog.write('User-initiated logout (Settings)'));
|
||||||
await client.logout().catchError((_) {});
|
await logoutWithoutStraySync(client).catchError((_) {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class _DeleteAccountDialogState extends ConsumerState<_DeleteAccountDialog> {
|
|||||||
}
|
}
|
||||||
// Konto wurde gelöscht — ausloggen
|
// Konto wurde gelöscht — ausloggen
|
||||||
unawaited(AuthLog.write('User-initiated logout (Konto gelöscht)'));
|
unawaited(AuthLog.write('User-initiated logout (Konto gelöscht)'));
|
||||||
try { await client.logout(); } catch (_) {}
|
try { await logoutWithoutStraySync(client); } catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class _LogoutNavButtonState extends ConsumerState<_LogoutNavButton> {
|
|||||||
try {
|
try {
|
||||||
widget.onClose();
|
widget.onClose();
|
||||||
unawaited(AuthLog.write('User-initiated logout (Settings-Sidebar)'));
|
unawaited(AuthLog.write('User-initiated logout (Settings-Sidebar)'));
|
||||||
await client.logout();
|
await logoutWithoutStraySync(client);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import 'package:pyramid/features/auth/bootstrap_dialog.dart' show isKeyBackupMis
|
|||||||
import 'package:pyramid/features/call_signaling/call_signaling_service.dart';
|
import 'package:pyramid/features/call_signaling/call_signaling_service.dart';
|
||||||
import 'package:pyramid/features/voice_channel/voice_channel_service.dart';
|
import 'package:pyramid/features/voice_channel/voice_channel_service.dart';
|
||||||
import 'package:pyramid/core/matrix_client.dart';
|
import 'package:pyramid/core/matrix_client.dart';
|
||||||
|
import 'package:pyramid/core/session_logout.dart';
|
||||||
import 'package:pyramid/core/settings_prefs.dart';
|
import 'package:pyramid/core/settings_prefs.dart';
|
||||||
import 'package:pyramid/core/theme.dart';
|
import 'package:pyramid/core/theme.dart';
|
||||||
import 'package:pyramid/core/update_checker.dart';
|
import 'package:pyramid/core/update_checker.dart';
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ if ($Live) {
|
|||||||
Invoke-Step "Live-Test: SQLCipher mit echtem Login (pyramidtest1)" {
|
Invoke-Step "Live-Test: SQLCipher mit echtem Login (pyramidtest1)" {
|
||||||
flutter test test/live_encryption_e2e_test.dart --concurrency=1
|
flutter test test/live_encryption_e2e_test.dart --concurrency=1
|
||||||
}
|
}
|
||||||
|
Invoke-Step "Live-Test: Logout + sofortiger Re-Login (SDK-Wettlauf)" {
|
||||||
|
flutter test test/live_logout_race_test.dart --concurrency=1
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
Remove-Item Env:PYRAMID_LIVE_TEST -ErrorAction SilentlyContinue
|
Remove-Item Env:PYRAMID_LIVE_TEST -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:matrix/matrix.dart';
|
|||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
import 'package:sqlite3/open.dart' as sqlite_open;
|
import 'package:sqlite3/open.dart' as sqlite_open;
|
||||||
import 'package:pyramid/core/app_database.dart';
|
import 'package:pyramid/core/app_database.dart';
|
||||||
|
import 'package:pyramid/core/session_logout.dart';
|
||||||
|
|
||||||
/// LIVE-End-to-End-Test der SQLCipher-Verschlüsselung mit einem ECHTEN
|
/// LIVE-End-to-End-Test der SQLCipher-Verschlüsselung mit einem ECHTEN
|
||||||
/// Matrix-Login gegen steggi-matrix.work (Test-Account, NICHT Bernd/Uta).
|
/// Matrix-Login gegen steggi-matrix.work (Test-Account, NICHT Bernd/Uta).
|
||||||
@@ -207,23 +208,6 @@ Future<void> _login(Client client, _TestAccounts acc,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Logout ohne Nachzügler. matrix 6.2 prüft bei einer FEHLER-Antwort des
|
|
||||||
/// Sync-Long-Polls nicht, ob die Anfrage noch aktuell ist: Läuft beim Logout
|
|
||||||
/// noch ein Long-Poll mit dem alten Token, kommt dessen 401 (M_UNKNOWN_TOKEN)
|
|
||||||
/// später zurück und das SDK ruft erneut `clear()`. Direkt nach einem
|
|
||||||
/// Re-Login würde das die NEUE Session löschen, nach `dispose()` wirft es
|
|
||||||
/// (DB geschlossen → Fallback auf die globale sqflite-Factory) – genau das
|
|
||||||
/// machte diesen Test sporadisch rot (2026-10-07). Deshalb erst die
|
|
||||||
/// Sync-Schleife anhalten, den laufenden Long-Poll regulär auslaufen lassen
|
|
||||||
/// (≤ 30 s), dann abmelden. `clear()` schaltet backgroundSync wieder ein.
|
|
||||||
Future<void> _logoutWithoutStraySync(Client client) async {
|
|
||||||
client.backgroundSync = false;
|
|
||||||
await client
|
|
||||||
.oneShotSync(timeout: Duration.zero)
|
|
||||||
.timeout(const Duration(seconds: 60));
|
|
||||||
await client.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holt (oder erzeugt) den unverschlüsselten Autopilot-Testraum.
|
/// Holt (oder erzeugt) den unverschlüsselten Autopilot-Testraum.
|
||||||
Future<Room> _ensureTestRoom(Client client) async {
|
Future<Room> _ensureTestRoom(Client client) async {
|
||||||
const roomName = 'Pyramid Autopilot Testraum';
|
const roomName = 'Pyramid Autopilot Testraum';
|
||||||
@@ -357,7 +341,7 @@ void main() {
|
|||||||
expect(client.userID?.toLowerCase(), acc.user1.toLowerCase());
|
expect(client.userID?.toLowerCase(), acc.user1.toLowerCase());
|
||||||
|
|
||||||
// ── Phase 4: Logout → Re-Login → alte Nachricht wieder lesbar ──
|
// ── Phase 4: Logout → Re-Login → alte Nachricht wieder lesbar ──
|
||||||
await _logoutWithoutStraySync(client);
|
await logoutWithoutStraySync(client); // App-Baustein (session_logout.dart)
|
||||||
expect(client.isLogged(), isFalse);
|
expect(client.isLogged(), isFalse);
|
||||||
await _login(client, acc,
|
await _login(client, acc,
|
||||||
user: acc.user1, pass: acc.pass1, device: 'Autopilot Enc-Test 2');
|
user: acc.user1, pass: acc.pass1, device: 'Autopilot Enc-Test 2');
|
||||||
@@ -365,7 +349,7 @@ void main() {
|
|||||||
expect(await _timelineContains(roomAfter, marker), isTrue,
|
expect(await _timelineContains(roomAfter, marker), isTrue,
|
||||||
reason: 'Alte Nachricht nach Logout+Re-Login nicht mehr lesbar');
|
reason: 'Alte Nachricht nach Logout+Re-Login nicht mehr lesbar');
|
||||||
// Aufräumen: Session serverseitig beenden, DB wird dabei geleert.
|
// Aufräumen: Session serverseitig beenden, DB wird dabei geleert.
|
||||||
await _logoutWithoutStraySync(client);
|
await logoutWithoutStraySync(client); // App-Baustein (session_logout.dart)
|
||||||
await client.dispose(closeDatabase: true);
|
await client.dispose(closeDatabase: true);
|
||||||
},
|
},
|
||||||
skip: skip,
|
skip: skip,
|
||||||
@@ -431,7 +415,7 @@ void main() {
|
|||||||
expect(roomAfter, isNotNull,
|
expect(roomAfter, isNotNull,
|
||||||
reason: 'Testraum nach Migration nicht mehr in der DB');
|
reason: 'Testraum nach Migration nicht mehr in der DB');
|
||||||
// Aufräumen (Session serverseitig beenden).
|
// Aufräumen (Session serverseitig beenden).
|
||||||
await _logoutWithoutStraySync(client);
|
await logoutWithoutStraySync(client); // App-Baustein (session_logout.dart)
|
||||||
await client.dispose(closeDatabase: true);
|
await client.dispose(closeDatabase: true);
|
||||||
},
|
},
|
||||||
skip: skip,
|
skip: skip,
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
@TestOn('windows')
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
import 'package:pyramid/core/session_logout.dart';
|
||||||
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
|
import 'package:sqlite3/open.dart' as sqlite_open;
|
||||||
|
|
||||||
|
/// LIVE-Test des SDK-Logout-Wettlaufs (ROADMAP M1, gefunden 2026-10-07) mit
|
||||||
|
/// dem Test-Account pyramidtest1 – gegated wie die anderen Live-Tests
|
||||||
|
/// (`PYRAMID_LIVE_TEST=1`), läuft nie im normalen `flutter test`.
|
||||||
|
///
|
||||||
|
/// Szenario wie in der App: Abmelden und SOFORT auf demselben Client-Objekt
|
||||||
|
/// wieder anmelden, dann 45 s beobachten. Kommt die 401 des alten Long-Polls
|
||||||
|
/// verspätet zurück, löscht das SDK die NEUE Session (`clear()`).
|
||||||
|
///
|
||||||
|
/// * „Nachweis“ (nur mit zusätzlich `PYRAMID_LIVE_RACE_PROOF=1`): der alte
|
||||||
|
/// Weg (direktes `client.logout()`) – zeigt, ob der Wettlauf auftritt.
|
||||||
|
/// * Regressionstest: `logoutWithoutStraySync` – die neue Session MUSS
|
||||||
|
/// eingeloggt bleiben.
|
||||||
|
|
||||||
|
Map<String, String>? _loadAccounts() {
|
||||||
|
final override = Platform.environment['PYRAMID_TEST_ACCOUNTS'];
|
||||||
|
final profile = Platform.environment['USERPROFILE'] ?? '';
|
||||||
|
final file =
|
||||||
|
File(override ?? '$profile\\.pyramid-autopilot\\test-accounts.txt');
|
||||||
|
if (!file.existsSync()) return null;
|
||||||
|
final map = <String, String>{};
|
||||||
|
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<Client> _newClient(String name) async {
|
||||||
|
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 client =
|
||||||
|
Client(name, database: await MatrixSdkDatabase.init(name, database: db));
|
||||||
|
await client.init(
|
||||||
|
waitForFirstSync: false, waitUntilLoadCompletedLoaded: false);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _login(Client client, Map<String, String> acc, String device) async {
|
||||||
|
await client.checkHomeserver(Uri.parse(acc['homeserver']!));
|
||||||
|
await client.login(
|
||||||
|
LoginType.mLoginPassword,
|
||||||
|
identifier: AuthenticationUserIdentifier(user: acc['user1']!),
|
||||||
|
password: acc['pass1']!,
|
||||||
|
initialDeviceDisplayName: device,
|
||||||
|
refreshToken: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Abmelden → sofort neu anmelden → [watch] lang beobachten. Liefert, ob die
|
||||||
|
/// NEUE Session dabei verloren ging.
|
||||||
|
Future<bool> _reloginLosesSession(
|
||||||
|
Map<String, String> acc,
|
||||||
|
Future<void> Function(Client) logout,
|
||||||
|
String label, {
|
||||||
|
Duration watch = const Duration(seconds: 45),
|
||||||
|
bool logoutRightAfterLogin = false,
|
||||||
|
}) async {
|
||||||
|
final client = await _newClient('PyramidRace$label');
|
||||||
|
try {
|
||||||
|
await _login(client, acc, 'Autopilot Race $label 1');
|
||||||
|
if (!logoutRightAfterLogin) {
|
||||||
|
// Warten, bis die Sync-Schleife im Long-Poll hängt (erster Sync hat
|
||||||
|
// timeout 0, danach 30-s-Long-Polls).
|
||||||
|
await client.oneShotSync().timeout(const Duration(seconds: 60));
|
||||||
|
await Future.delayed(const Duration(seconds: 3));
|
||||||
|
}
|
||||||
|
// Sonst: sofort abmelden, während die Schleife nach dem ersten (schnellen)
|
||||||
|
// Sync gerade neue Anfragen mit dem alten Token losschickt.
|
||||||
|
|
||||||
|
await logout(client);
|
||||||
|
expect(client.isLogged(), isFalse);
|
||||||
|
|
||||||
|
await _login(client, acc, 'Autopilot Race $label 2');
|
||||||
|
final newToken = client.accessToken;
|
||||||
|
var lost = false;
|
||||||
|
final sub = client.onLoginStateChanged.stream.listen((s) {
|
||||||
|
if (s == LoginState.loggedOut) lost = true;
|
||||||
|
});
|
||||||
|
final end = DateTime.now().add(watch);
|
||||||
|
while (DateTime.now().isBefore(end) && !lost) {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
await sub.cancel();
|
||||||
|
lost = lost || !client.isLogged() || client.accessToken != newToken;
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[$label] neue Session nach ${watch.inSeconds}s '
|
||||||
|
'${lost ? 'VERLOREN' : 'noch eingeloggt'}');
|
||||||
|
return lost;
|
||||||
|
} finally {
|
||||||
|
if (client.isLogged()) {
|
||||||
|
try {
|
||||||
|
await logoutWithoutStraySync(client);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
await client.dispose(closeDatabase: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final acc = _loadAccounts();
|
||||||
|
final skip = Platform.environment['PYRAMID_LIVE_TEST'] != '1'
|
||||||
|
? 'Live-Test nur mit PYRAMID_LIVE_TEST=1 (echter Homeserver!)'
|
||||||
|
: acc == null
|
||||||
|
? 'Test-Account-Datei fehlt'
|
||||||
|
: _findSqliteDll() == null
|
||||||
|
? 'SQLite-DLL fehlt (erst flutter build windows)'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (final immediately in [false, true]) {
|
||||||
|
final when = immediately ? 'direkt nach dem Login' : 'nach Leerlauf';
|
||||||
|
final tag = immediately ? 'sofort' : 'leerlauf';
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Nachweis ($when): direktes logout() + sofortiger Re-Login (alter Weg)',
|
||||||
|
() async {
|
||||||
|
final lost = await _reloginLosesSession(
|
||||||
|
acc!,
|
||||||
|
(c) => c.logout(),
|
||||||
|
'alt-$tag',
|
||||||
|
logoutRightAfterLogin: immediately,
|
||||||
|
);
|
||||||
|
// Kein expect: dokumentiert nur, ob der Wettlauf auftritt.
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('Wettlauf ($when) mit altem Weg: ${lost ? 'JA' : 'nein'}');
|
||||||
|
},
|
||||||
|
skip: skip ??
|
||||||
|
(Platform.environment['PYRAMID_LIVE_RACE_PROOF'] != '1'
|
||||||
|
? 'Nachweis nur mit PYRAMID_LIVE_RACE_PROOF=1'
|
||||||
|
: null),
|
||||||
|
timeout: const Timeout(Duration(minutes: 4)),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'logoutWithoutStraySync ($when): neue Session bleibt nach sofortigem '
|
||||||
|
'Re-Login',
|
||||||
|
() async {
|
||||||
|
final lost = await _reloginLosesSession(
|
||||||
|
acc!,
|
||||||
|
logoutWithoutStraySync,
|
||||||
|
'neu-$tag',
|
||||||
|
logoutRightAfterLogin: immediately,
|
||||||
|
);
|
||||||
|
expect(lost, isFalse,
|
||||||
|
reason: 'verspätete 401 einer alten Sync-Anfrage hat die neue '
|
||||||
|
'Session gelöscht');
|
||||||
|
},
|
||||||
|
skip: skip,
|
||||||
|
timeout: const Timeout(Duration(minutes: 4)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user