Files
Melo/test/services/baka_auth_test.dart
T
Hermes (Server) 609ca37ede Auto-Anmeldung: doppelten Login-Request beim App-Start verhindert (Race durch IndexedStack)
Betraf alle drei Nutzer (Dustin, Baka, Tinker), nicht nur einen: IndexedStack
baut Download-Tab und YT-Suche beim App-Start gleichzeitig, beide loesen
unabhaengig voneinander autoAnmelden() aus. Ohne In-Flight-Absicherung gingen
zwei fast-gleichzeitige Login-Requests an denselben Server, von denen einer
abgelehnt wurde - obwohl die Zugangsdaten korrekt waren.

- BakaAuth.autoAnmelden() haengt einen ueberlappenden Aufruf jetzt an den
  bereits laufenden Login-Versuch an, statt einen zweiten Request zu senden.
- Anmelde-Dialog im Download-Tab zeigt den tatsaechlich bekannten
  Navidrome-Benutzernamen statt immer "Baka" vorauszufuellen.
- Navidrome-Dialog in den Einstellungen waehlt den zuletzt bekannten
  Benutzer vor statt immer "Baka".
2026-08-29 11:31:28 +02:00

175 lines
5.7 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:melo/services/baka_auth.dart';
/// Einfacher Speicher im Arbeitsspeicher — FlutterSecureStorage braucht
/// Plattform-Kanäle und ist im Test nicht verfügbar.
class _MemorySpeicher implements TokenSpeicher {
final Map<String, String> werte = {};
@override
Future<String?> lesen(String key) async => werte[key];
@override
Future<void> schreiben(String key, String wert) async => werte[key] = wert;
@override
Future<void> loeschen(String key) async => werte.remove(key);
}
void main() {
test('Anmeldung mit gültigen Daten speichert den Token', () async {
final speicher = _MemorySpeicher();
late String gesendet;
final client = MockClient((req) async {
gesendet = req.body;
return http.Response(jsonEncode({'token': 'jwt-abc'}), 200);
});
final auth = BakaAuth(client: client, speicher: speicher);
final fehler = await auth.anmelden('Baka', 'geheim');
expect(fehler, isNull);
expect(auth.istAngemeldet, isTrue);
expect(auth.benutzer, 'Baka');
expect(speicher.werte['baka_token'], 'jwt-abc');
expect(jsonDecode(gesendet), {'username': 'Baka', 'password': 'geheim'});
});
test('Auth-Header trägt den Token als Bearer', () async {
final client = MockClient(
(_) async => http.Response(jsonEncode({'token': 'jwt-abc'}), 200));
final auth = BakaAuth(client: client, speicher: _MemorySpeicher());
await auth.anmelden('Baka', 'geheim');
expect(auth.authHeader['Authorization'], 'Bearer jwt-abc');
});
test('Falsches Passwort liefert die Server-Meldung, obwohl HTTP 200 kommt',
() async {
// Der Baka-Auth-Server antwortet auch bei Fehlern mit Status 200.
final client = MockClient((_) async => http.Response(
jsonEncode({'status': 'error', 'message': 'Falscher Benutzer'}), 200));
final auth = BakaAuth(client: client, speicher: _MemorySpeicher());
final fehler = await auth.anmelden('Baka', 'falsch');
expect(fehler, 'Falscher Benutzer');
expect(auth.istAngemeldet, isFalse);
});
test('Nicht erreichbarer Server meldet einen Verbindungsfehler', () async {
final client = MockClient((_) async => throw const SocketAusfall());
final auth = BakaAuth(client: client, speicher: _MemorySpeicher());
final fehler = await auth.anmelden('Baka', 'geheim');
expect(fehler, 'Keine Verbindung zum Anmelde-Server');
expect(auth.istAngemeldet, isFalse);
});
test('laden() stellt einen gespeicherten Token wieder her', () async {
final speicher = _MemorySpeicher();
speicher.werte['baka_token'] = 'jwt-alt';
speicher.werte['baka_user'] = 'Baka';
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: speicher);
await auth.laden();
expect(auth.istAngemeldet, isTrue);
expect(auth.benutzer, 'Baka');
});
test('autoAnmelden meldet mit den übergebenen Zugangsdaten an', () async {
late String gesendet;
final client = MockClient((req) async {
gesendet = req.body;
return http.Response(jsonEncode({'token': 'jwt-server'}), 200);
});
final auth = BakaAuth(client: client, speicher: _MemorySpeicher());
await auth.autoAnmelden('Tinker', 'geheim2');
expect(auth.istAngemeldet, isTrue);
expect(auth.benutzer, 'Tinker');
expect(jsonDecode(gesendet), {'username': 'Tinker', 'password': 'geheim2'});
});
test('autoAnmelden greift nicht ein, wenn schon angemeldet', () async {
var aufrufe = 0;
final client = MockClient((_) async {
aufrufe++;
return http.Response(jsonEncode({'token': 'jwt-abc'}), 200);
});
final auth = BakaAuth(client: client, speicher: _MemorySpeicher());
await auth.anmelden('Baka', 'geheim');
await auth.autoAnmelden('Tinker', 'geheim2');
expect(aufrufe, 1);
expect(auth.benutzer, 'Baka');
});
test(
'autoAnmelden löst bei gleichzeitigen Aufrufen nur EINEN HTTP-Request aus '
'(Race Condition durch IndexedStack, das mehrere Tabs gleichzeitig baut)',
() async {
var aufrufe = 0;
final client = MockClient((_) async {
aufrufe++;
// Künstliche Verzögerung: simuliert, dass der erste Request noch
// nicht zurück ist, wenn der zweite Aufruf startet.
await Future.delayed(const Duration(milliseconds: 50));
return http.Response(jsonEncode({'token': 'jwt-tinker'}), 200);
});
final auth = BakaAuth(client: client, speicher: _MemorySpeicher());
await Future.wait([
auth.autoAnmelden('Tinker', 'geheim'),
auth.autoAnmelden('Tinker', 'geheim'),
]);
expect(aufrufe, 1);
expect(auth.istAngemeldet, isTrue);
expect(auth.benutzer, 'Tinker');
});
test('autoAnmelden tut nichts ohne Zugangsdaten', () async {
var aufrufe = 0;
final client = MockClient((_) async {
aufrufe++;
return http.Response(jsonEncode({'token': 'jwt'}), 200);
});
final auth = BakaAuth(client: client, speicher: _MemorySpeicher());
await auth.autoAnmelden('', '');
expect(aufrufe, 0);
expect(auth.istAngemeldet, isFalse);
});
test('Abmelden löscht Token und Benutzer aus dem Speicher', () async {
final speicher = _MemorySpeicher();
final auth = BakaAuth(
client: MockClient(
(_) async => http.Response(jsonEncode({'token': 'jwt-abc'}), 200)),
speicher: speicher);
await auth.anmelden('Baka', 'geheim');
await auth.abmelden();
expect(auth.istAngemeldet, isFalse);
expect(speicher.werte, isEmpty);
});
}
/// Steht im Test für einen Netzwerkausfall.
class SocketAusfall implements Exception {
const SocketAusfall();
}