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/cloud_service_test.dart
T
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

289 lines
9.2 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/cloud_service.dart';
/// CloudService-Tests mit http-Mocks (package:http/testing.dart — Teil von
/// http, kein neues Package). Abgedeckt: LWW-Sync-Konflikt-Pfade (updated_at
/// bleibt für den Client-Vergleich erhalten), Auth-Fehler (401) und
/// Netzwerkfehler (alle Pfade failen sauber statt zu crashen).
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
http.Response jsonOk(Map<String, dynamic> body) =>
http.Response(jsonEncode(body), 200,
headers: {'content-type': 'application/json'});
group('login (Auth)', () {
test('200 → verbunden (true)', () async {
final cloud = CloudService(
client: MockClient((req) async => http.Response('{}', 200)),
);
expect(await cloud.login('dustin'), isTrue);
});
test('401 (Auth-Fehler) → false', () async {
final cloud = CloudService(
client: MockClient((req) async => http.Response('{}', 401)),
);
expect(await cloud.login('dustin'), isFalse);
});
test('Netzwerkfehler → false (kein Crash)', () async {
final cloud = CloudService(
client: MockClient(
(req) async => throw http.ClientException('Verbindung weg')),
);
expect(await cloud.login('dustin'), isFalse);
});
});
group('verbinde()', () {
test('ohne Benutzer → Status fehler (kein Request)', () async {
var requests = 0;
final cloud = CloudService(
client: MockClient((req) async {
requests++;
return http.Response('{}', 200);
}),
);
await cloud.verbinde();
expect(cloud.verbindungGestartet, isTrue);
expect(cloud.status, CloudStatus.fehler);
expect(cloud.istVerbunden, isFalse);
expect(requests, 0);
});
});
group('statusDaten (Auth-Fehler)', () {
test('200 → JSON-Map', () async {
final cloud = CloudService(
client: MockClient(
(req) async => jsonOk({'songs': 5, 'users': 2})),
);
final d = await cloud.statusDaten();
expect(d, isNotNull);
expect(d!['songs'], 5);
});
test('401 → null (Token abgelaufen)', () async {
final cloud = CloudService(
client: MockClient((req) async => http.Response('{}', 401)),
);
expect(await cloud.statusDaten(), isNull);
});
test('Netzwerkfehler → null', () async {
final cloud = CloudService(
client: MockClient(
(req) async => throw http.ClientException('offline')),
);
expect(await cloud.statusDaten(), isNull);
});
});
group('syncChanges — LWW-Sync-Konflikt-Pfade', () {
test('parst Changes inkl. updated_at (Grundlage für LWW-Vergleich)',
() async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({
'changes': [
{
'id': 'song-1',
'title': 'Neu vom Server',
'updated_at': '2026-08-04T10:00:00Z',
},
{
'id': 'song-2',
'title': 'Älterer Eintrag',
'updated_at': '2026-08-04T09:00:00Z',
},
],
})),
);
final changes = await cloud.syncChanges('2026-08-04T08:00:00Z');
expect(changes, hasLength(2));
// LWW-Vertrag: Timestamps bleiben unverändert erhalten, damit der
// Client lokal neueren Stand (Upload via syncAll) von server-neuerem
// Stand (Übernahme) unterscheiden kann.
expect(changes[0]['updated_at'], '2026-08-04T10:00:00Z');
expect(changes[1]['updated_at'], '2026-08-04T09:00:00Z');
expect(changes[0]['title'], 'Neu vom Server');
});
test('leere Changes-Liste → []', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({'changes': []})),
);
expect(await cloud.syncChanges('2026-08-04T08:00:00Z'), isEmpty);
});
test('fehlendes changes-Feld → []', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({})),
);
expect(await cloud.syncChanges('2026-08-04T08:00:00Z'), isEmpty);
});
test('401 (Auth-Fehler) → []', () async {
final cloud = CloudService(
client: MockClient((req) async => http.Response('{}', 401)),
);
expect(await cloud.syncChanges('2026-08-04T08:00:00Z'), isEmpty);
});
test('Netzwerkfehler → []', () async {
final cloud = CloudService(
client: MockClient(
(req) async => throw http.ClientException('offline')),
);
expect(await cloud.syncChanges('2026-08-04T08:00:00Z'), isEmpty);
});
test('sendet since-Timestamp als Body', () async {
late Map<String, dynamic> gesendeterBody;
final cloud = CloudService(
client: MockClient((req) async {
gesendeterBody = jsonDecode(req.body) as Map<String, dynamic>;
return jsonOk({'changes': []});
}),
);
await cloud.syncChanges('2026-08-04T08:00:00Z');
expect(gesendeterBody['since'], '2026-08-04T08:00:00Z');
});
});
group('syncAll (LWW-Schreibpfad)', () {
test('200 → Status-Map', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({'status': 'ok'})),
);
final d = await cloud.syncAll();
expect(d?['status'], 'ok');
});
test('401 → null (Auth-Fehler)', () async {
final cloud = CloudService(
client: MockClient((req) async => http.Response('{}', 401)),
);
expect(await cloud.syncAll(), isNull);
});
});
group('Playlisten', () {
test('getPlaylists parst Liste', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({
'playlists': [
{'id': 1, 'name': 'Workout'},
],
})),
);
final list = await cloud.getPlaylists();
expect(list, hasLength(1));
expect(list.first['name'], 'Workout');
});
test('createPlaylist 200 → Playlist-Objekt', () async {
final cloud = CloudService(
client: MockClient((req) async =>
jsonOk({'playlist': {'id': 7, 'name': 'Neu'}})),
);
final p = await cloud.createPlaylist('Neu');
expect(p?['id'], 7);
});
test('deletePlaylist 200 ohne status → false', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({'status': 'ok'})),
);
expect(await cloud.deletePlaylist(3), isTrue);
});
});
group('Favoriten', () {
test('getFavorites parst Liste', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({
'favorites': [
{'id': 's1'},
],
})),
);
final list = await cloud.getFavorites();
expect(list, hasLength(1));
expect(list.first['id'], 's1');
});
test('syncFavorites mit status ok → true', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({'status': 'ok'})),
);
expect(await cloud.syncFavorites(['s1', 's2']), isTrue);
});
test('syncFavorites 401 → false (Auth-Fehler)', () async {
final cloud = CloudService(
client: MockClient((req) async => http.Response('{}', 401)),
);
expect(await cloud.syncFavorites(['s1']), isFalse);
});
});
group('History & Songs', () {
test('getHistory parst history-Liste', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({
'history': [
{'song_id': 's1', 'played_at': '2026-08-04T10:00:00Z'},
],
})),
);
final list = await cloud.getHistory();
expect(list, hasLength(1));
expect(list.first['song_id'], 's1');
});
test('listSongs parst songs-Liste', () async {
final cloud = CloudService(
client: MockClient((req) async => jsonOk({
'songs': [
{'id': 's1', 'title': 'Titel'},
],
})),
);
final list = await cloud.listSongs();
expect(list, hasLength(1));
expect(list.first['title'], 'Titel');
});
});
group('delete / globalList (Fehlerpfade)', () {
test('delete 200 → true, 401 → false', () async {
final cloudOk = CloudService(
client: MockClient((req) async => http.Response('{}', 200)),
);
expect(await cloudOk.delete('s1'), isTrue);
final cloud401 = CloudService(
client: MockClient((req) async => http.Response('{}', 401)),
);
expect(await cloud401.delete('s1'), isFalse);
});
test('globalList 200 → Songs, 401 → []', () async {
final cloudOk = CloudService(
client: MockClient((req) async =>
jsonOk({'songs': [{'id': 'g1'}]})),
);
expect(await cloudOk.globalList(), hasLength(1));
final cloud401 = CloudService(
client: MockClient((req) async => http.Response('{}', 401)),
);
expect(await cloud401.globalList(), isEmpty);
});
});
}