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_app/services/auth_service.dart'; /// AuthService-Tests mit http-Mocks (package:http/testing.dart). /// Abgedeckt: Token-Ablauf-Pfade (401 → Logout + Speicher löschen), /// Netzwerkfehler → Offline-Trust (Token behalten), 5xx ≠ 401, Login-Pfade. /// In-Memory-TokenSpeicher (ersetzt FlutterSecureStorage im Test). class FakeSpeicher implements TokenSpeicher { final Map daten = {}; @override Future lesen(String key) async => daten[key]; @override Future schreiben(String key, String? wert) async { if (wert == null) { daten.remove(key); } else { daten[key] = wert; } } @override Future loeschen(String key) async => daten.remove(key); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); /// MockClient, der /verify mit [verifyStatus] beantwortet. /// [verifyStatus] == -1 → Netzwerkfehler (ClientException). MockClient verifyClient(int verifyStatus) => MockClient((req) async { if (req.url.path.endsWith('/verify')) { if (verifyStatus == -1) { throw http.ClientException('Verbindung fehlgeschlagen'); } return http.Response('{}', verifyStatus); } return http.Response('{}', 404); }); AuthService neueInstanz(MockClient client, FakeSpeicher speicher) => AuthService.fuerTests(client: client, speicher: speicher); group('initialisieren — Token-Ablauf-Pfade', () { test('gültiger Token (200) → eingeloggt, Token bleibt', () async { final speicher = FakeSpeicher() ..daten['baka_token'] = 'jwt.valid' ..daten['baka_user'] = 'dustin'; final auth = neueInstanz(verifyClient(200), speicher); expect(await auth.initialisieren(), isTrue); expect(auth.istEingeloggt, isTrue); expect(auth.benutzer, 'dustin'); expect(speicher.daten['baka_token'], 'jwt.valid'); // persistiert }); test('401 (Token abgelaufen) → ausgeloggt, Token + User gelöscht', () async { final speicher = FakeSpeicher() ..daten['baka_token'] = 'jwt.abgelaufen' ..daten['baka_user'] = 'dustin'; final auth = neueInstanz(verifyClient(401), speicher); expect(await auth.initialisieren(), isFalse); expect(auth.istEingeloggt, isFalse); expect(auth.benutzer, ''); expect(speicher.daten.containsKey('baka_token'), isFalse); expect(speicher.daten.containsKey('baka_user'), isFalse); }); test('Netzwerkfehler → Offline-Trust: Token bleibt, eingeloggt', () async { final speicher = FakeSpeicher() ..daten['baka_token'] = 'jwt.offline' ..daten['baka_user'] = 'dustin'; final auth = neueInstanz(verifyClient(-1), speicher); expect(await auth.initialisieren(), isTrue); expect(auth.istEingeloggt, isTrue); expect(speicher.daten['baka_token'], 'jwt.offline'); // NICHT gelöscht }); test('5xx (Server-Fehler) ≠ 401 → Token bleibt', () async { final speicher = FakeSpeicher() ..daten['baka_token'] = 'jwt.server500' ..daten['baka_user'] = 'dustin'; final auth = neueInstanz(verifyClient(500), speicher); expect(await auth.initialisieren(), isTrue); expect(speicher.daten['baka_token'], 'jwt.server500'); }); test('ohne gespeicherten Token → false, kein Verify-Request', () async { var verifyRequests = 0; final client = MockClient((req) async { verifyRequests++; return http.Response('{}', 200); }); final auth = neueInstanz(client, FakeSpeicher()); expect(await auth.initialisieren(), isFalse); expect(verifyRequests, 0); }); }); group('tokenPruefen', () { /// Client, der /login mit Token beantwortet (seedt _token) UND /verify /// mit [verifyStatus] (verifyStatus == -1 → Netzwerkfehler). MockClient tokenClient(int verifyStatus) => MockClient((req) async { if (req.url.path.endsWith('/login')) { return http.Response(jsonEncode({'token': 'jwt.test'}), 200); } if (req.url.path.endsWith('/verify')) { if (verifyStatus == -1) { throw http.ClientException('Verbindung fehlgeschlagen'); } return http.Response('{}', verifyStatus); } return http.Response('{}', 404); }); Future eingeloggteInstanz(int verifyStatus) async { final auth = neueInstanz(tokenClient(verifyStatus), FakeSpeicher()); await auth.login('dustin', 'passwort'); // setzt _token return auth; } test('200 → true', () async { final auth = await eingeloggteInstanz(200); expect(await auth.tokenPruefen(), isTrue); }); test('401 → false (abgelaufen)', () async { final auth = await eingeloggteInstanz(401); expect(await auth.tokenPruefen(), isFalse); }); test('Netzwerkfehler → true (Offline-Trust)', () async { final auth = await eingeloggteInstanz(-1); expect(await auth.tokenPruefen(), isTrue); }); test('503 → true (temporärer Server-Fehler ≠ Logout)', () async { final auth = await eingeloggteInstanz(503); expect(await auth.tokenPruefen(), isTrue); }); }); group('login', () { MockClient loginClient(int status, {String? token}) => MockClient((req) async { if (req.url.path.endsWith('/login')) { if (status == -1) throw http.ClientException('offline'); return http.Response( jsonEncode({if (token != null) 'token': token}), status, ); } return http.Response('{}', 404); }); test('200 mit Token → ok, Token + User persistiert', () async { final speicher = FakeSpeicher(); final auth = neueInstanz(loginClient(200, token: 'jwt.frisch'), speicher); final result = await auth.login('dustin', 'passwort'); expect(result.erfolg, isTrue); expect(auth.istEingeloggt, isTrue); expect(auth.benutzer, 'dustin'); expect(speicher.daten['baka_token'], 'jwt.frisch'); expect(speicher.daten['baka_user'], 'dustin'); }); test('401 (falsches Passwort) → fehlgeschlagen', () async { final auth = neueInstanz(loginClient(401), FakeSpeicher()); final result = await auth.login('dustin', 'falsch'); expect(result.erfolg, isFalse); expect(result.fehler, contains('401')); expect(auth.istEingeloggt, isFalse); }); test('200 OHNE Token → fehlgeschlagen (Fall-Through-Bug-Regression)', () async { final auth = neueInstanz(loginClient(200), FakeSpeicher()); final result = await auth.login('dustin', 'passwort'); expect(result.erfolg, isFalse); expect(auth.istEingeloggt, isFalse); }); test('Netzwerkfehler → fehlgeschlagen mit Server-Meldung', () async { final auth = neueInstanz(loginClient(-1), FakeSpeicher()); final result = await auth.login('dustin', 'passwort'); expect(result.erfolg, isFalse); expect(result.fehler, contains('Keine Verbindung')); }); }); group('logout & authHeader', () { test('logout löscht Token + User aus Speicher', () async { final speicher = FakeSpeicher() ..daten['baka_token'] = 'jwt.x' ..daten['baka_user'] = 'dustin'; final auth = neueInstanz(verifyClient(200), speicher); await auth.initialisieren(); // stellt Token her expect(auth.istEingeloggt, isTrue); await auth.logout(); expect(auth.istEingeloggt, isFalse); expect(auth.benutzer, ''); expect(speicher.daten.containsKey('baka_token'), isFalse); expect(speicher.daten.containsKey('baka_user'), isFalse); }); test('authHeader enthält Bearer-Token nach Login', () async { final speicher = FakeSpeicher(); final auth = neueInstanz( MockClient((req) async => http.Response(jsonEncode({'token': 'jwt.hdr'}), 200)), speicher, ); await auth.login('dustin', 'passwort'); final header = auth.authHeader; expect(header['Authorization'], 'Bearer jwt.hdr'); expect(header['Content-Type'], 'application/json'); }); test('authHeader ohne Token → kein Authorization', () async { final auth = neueInstanz(verifyClient(200), FakeSpeicher()); expect(auth.authHeader.containsKey('Authorization'), isFalse); }); }); }