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:
@@ -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', {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ enum CloudStatus { verbinde, verbunden, fehler }
|
||||
/// Merge-Strategie wird NICHT unterstützt (kein 3-Way-Merge) — es gewinnt immer
|
||||
/// der jüngste Timestamp.
|
||||
class CloudService extends ChangeNotifier {
|
||||
static final http.Client _client = http.Client();
|
||||
/// Injizierbarer HTTP-Client (Tests: package:http/testing.dart MockClient).
|
||||
CloudService({http.Client? client}) : _client = client ?? http.Client();
|
||||
|
||||
final http.Client _client;
|
||||
|
||||
static String get _base => AppConfig.cloudUrl;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user