This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/test/auth_service_test.dart
Dustin 5b5f72b548 v2.50.2 — Cloud/Auth-Tests (http-Mocks): LWW-Sync + Token-Ablauf-Pfade
## Testbarkeit (kein neues Package)
- CloudService: http.Client injizierbar (Konstruktor-Parameter, Default unverändert)
- AuthService: http.Client + TokenSpeicher injizierbar; SecureTokenSpeicher kapselt FlutterSecureStorage, @visibleForTesting AuthService.fuerTests
- Alle HTTP-Calls über _client statt Top-Level http.get/post

## test/cloud_service_test.dart (22 Tests)
- login: 200/401/Netzwerkfehler
- verbinde() ohne Benutzer → Status fehler, kein Request
- statusDaten: 200/401(Auth-Fehler)/Netzwerkfehler
- syncChanges LWW-Konflikt-Pfade: updated_at bleibt für Client-Vergleich erhalten, leere/fehlende Changes, 401 → [], Netzwerkfehler → [], since-Body
- syncAll (LWW-Schreibpfad): 200/401
- Playlists, Favoriten, History, listSongs, delete, globalList inkl. Fehlerpfade

## test/auth_service_test.dart (19 Tests)
- initialisieren: 200 → eingeloggt, 401 → Logout + Speicher gelöscht, Netzwerkfehler → Offline-Trust, 5xx → Token bleibt, ohne Token → kein Request
- tokenPruefen: 200/401/Netzwerkfehler/503
- login: ok mit Persistenz, 401, 200-ohne-Token (Fall-Through-Regression), Netzwerkfehler
- logout löscht Speicher, authHeader mit/ohne Bearer
2026-08-04 20:46:51 +02:00

236 lines
8.4 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_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<String, String> daten = {};
@override
Future<String?> lesen(String key) async => daten[key];
@override
Future<void> schreiben(String key, String? wert) async {
if (wert == null) {
daten.remove(key);
} else {
daten[key] = wert;
}
}
@override
Future<void> 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<AuthService> 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);
});
});
}