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
This commit is contained in:
Dustin
2026-08-04 20:46:51 +02:00
parent e584a7d672
commit 5b5f72b548
4 changed files with 574 additions and 16 deletions
+47 -15
View File
@@ -1,17 +1,49 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../config/app_config.dart';
import '../services/melo_logger.dart';
/// Minimal-Speicherabstraktion für die Token-Persistenz — macht den Service
/// in Tests mit einem In-Memory-Fake testbar (kein Platform-Channel nötig).
abstract class TokenSpeicher {
Future<String?> lesen(String key);
Future<void> schreiben(String key, String? wert);
Future<void> loeschen(String key);
}
/// Produktions-Implementierung auf FlutterSecureStorage.
class SecureTokenSpeicher implements TokenSpeicher {
final FlutterSecureStorage _secure = const FlutterSecureStorage();
@override
Future<String?> lesen(String key) => _secure.read(key: key);
@override
Future<void> schreiben(String key, String? wert) =>
_secure.write(key: key, value: wert);
@override
Future<void> loeschen(String key) => _secure.delete(key: key);
}
/// Baka-Auth Service JWT-basierte Authentifizierung (v3 — Persistent Login)
/// Integriert mit https://baka-net.de/auth
class AuthService {
static final AuthService _instance = AuthService._();
factory AuthService() => _instance;
AuthService._();
AuthService._({http.Client? client, TokenSpeicher? speicher})
: _client = client ?? http.Client(),
_speicher = speicher ?? SecureTokenSpeicher();
final FlutterSecureStorage _secure = const FlutterSecureStorage();
/// Nur für Tests: frische Instanz mit injizierbaren Abhängigkeiten.
@visibleForTesting
AuthService.fuerTests({http.Client? client, TokenSpeicher? speicher})
: this._(client: client, speicher: speicher);
final http.Client _client;
final TokenSpeicher _speicher;
String? _token;
String _user = '';
@@ -36,8 +68,8 @@ class AuthService {
Future<bool> initialisieren() async {
if (_initialisiert) return istEingeloggt;
try {
_token = await _secure.read(key: 'baka_token');
_user = await _secure.read(key: 'baka_user') ?? '';
_token = await _speicher.lesen('baka_token');
_user = await _speicher.lesen('baka_user') ?? '';
if (_token != null && _token!.isNotEmpty) {
// Token beim Server validieren (nicht blind vertrauen)
@@ -47,8 +79,8 @@ class AuthService {
MeloLogger().zustand('auth_token_expired', {'user': _user});
_token = null;
_user = '';
await _secure.delete(key: 'baka_token');
await _secure.delete(key: 'baka_user');
await _speicher.loeschen('baka_token');
await _speicher.loeschen('baka_user');
} else {
MeloLogger().zustand('auth_restored', {'user': _user});
}
@@ -64,7 +96,7 @@ class AuthService {
Future<bool> _tokenOnlinePruefen() async {
if (_token == null) return false;
try {
final response = await http
final response = await _client
.get(
Uri.parse('${AppConfig.authUrl}/verify'),
headers: authHeader,
@@ -84,7 +116,7 @@ class AuthService {
/// Login über Baka-Auth-Server
Future<AuthResult> login(String user, String password) async {
try {
final response = await http
final response = await _client
.post(
Uri.parse('${AppConfig.authUrl}/login'),
headers: {'Content-Type': 'application/json'},
@@ -101,8 +133,8 @@ class AuthService {
_user = user;
if (_token != null && _token!.isNotEmpty) {
await _secure.write(key: 'baka_token', value: _token!);
await _secure.write(key: 'baka_user', value: _user);
await _speicher.schreiben('baka_token', _token!);
await _speicher.schreiben('baka_user', _user);
MeloLogger().aktion('auth_login_ok', {'user': _user});
return AuthResult.ok;
}
@@ -126,7 +158,7 @@ class AuthService {
Future<AuthResult> registrieren(
String user, String password, String email) async {
try {
final response = await http
final response = await _client
.post(
Uri.parse('${AppConfig.authUrl}/register'),
headers: {'Content-Type': 'application/json'},
@@ -144,8 +176,8 @@ class AuthService {
_user = user;
if (_token != null && _token!.isNotEmpty) {
await _secure.write(key: 'baka_token', value: _token!);
await _secure.write(key: 'baka_user', value: _user);
await _speicher.schreiben('baka_token', _token!);
await _speicher.schreiben('baka_user', _user);
MeloLogger().aktion('auth_register_ok', {'user': _user});
return AuthResult.ok;
}
@@ -171,8 +203,8 @@ class AuthService {
Future<void> logout() async {
_token = null;
_user = '';
await _secure.delete(key: 'baka_token');
await _secure.delete(key: 'baka_user');
await _speicher.loeschen('baka_token');
await _speicher.loeschen('baka_user');
MeloLogger().aktion('auth_logout', {});
}
}