Files
Melo/test/services/melo_cloud_service_test.dart

216 lines
6.5 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/services/baka_auth.dart';
import 'package:melo/services/melo_cloud_service.dart';
class _MemorySpeicher implements TokenSpeicher {
_MemorySpeicher(this.werte);
final Map<String, String> werte;
@override
Future<String?> lesen(String key) async => werte[key];
@override
Future<void> schreiben(String key, String wert) async => werte[key] = wert;
@override
Future<void> loeschen(String key) async => werte.remove(key);
}
Future<BakaAuth> _angemeldeteAuth() async {
final auth = BakaAuth(
speicher: _MemorySpeicher({'baka_token': 'tok', 'baka_user': 'Baka'}),
);
await auth.laden();
return auth;
}
void main() {
group('parseListe', () {
test('liest Titel inklusive Grabsteinen', () {
final body = jsonEncode({
'status': 'ok',
'songs': [
{
'id': 'abc',
'title': 'Nachtpuls',
'artist': 'Rotklang',
'duration': 210,
'size': 4200,
'deleted': false,
},
{'id': 'def', 'title': 'Weg', 'deleted': true},
],
'count': 1,
});
final songs = MeloCloudService.parseListe(body);
expect(songs, hasLength(2));
expect(songs.first.id, 'abc');
expect(songs.first.kuenstler, 'Rotklang');
expect(songs.first.dauerSekunden, 210);
expect(songs.first.geloescht, isFalse);
expect(songs.last.geloescht, isTrue);
});
test('leere Liste ist kein Fehler', () {
expect(
MeloCloudService.parseListe(jsonEncode({'status': 'ok', 'songs': []})),
isEmpty,
);
});
test('Server-Fehler wird als CloudException gemeldet', () {
expect(
() => MeloCloudService.parseListe(jsonEncode({'error': 'Auth required'})),
throwsA(isA<CloudException>()),
);
});
test('fehlender Titel wird nicht zu einer leeren Zeile', () {
final songs = MeloCloudService.parseListe(jsonEncode({
'songs': [
{'id': 'x', 'title': ''}
]
}));
expect(songs.single.titel, 'Unbekannt');
});
});
group('parseUpload', () {
test('liefert die vergebene Server-ID', () {
final body = jsonEncode(
{'status': 'ok', 'song_id': 'neu123', 'title': 'A', 'action': 'new'});
expect(MeloCloudService.parseUpload(body), 'neu123');
});
test('erkennt eine Dublette an derselben ID', () {
// Der Server verknüpft dieselbe Datei mit dem bestehenden Titel,
// statt eine zweite Kopie anzulegen.
final body = jsonEncode(
{'status': 'ok', 'song_id': 'alt999', 'action': 'linked'});
expect(MeloCloudService.parseUpload(body), 'alt999');
});
test('Fehler wird als CloudException gemeldet', () {
expect(
() => MeloCloudService.parseUpload(
jsonEncode({'error': 'Datei zu groß (max 50 MB)'})),
throwsA(isA<CloudException>()),
);
});
});
group('parseFavoriten', () {
test('liefert nur die IDs', () {
final body = jsonEncode({
'favorites': [
{'id': 'a', 'title': 'A'},
{'id': 'b', 'title': 'B'},
]
});
expect(MeloCloudService.parseFavoriten(body), ['a', 'b']);
});
test('eine leere Favoritenliste ist kein Fehler', () {
expect(
MeloCloudService.parseFavoriten(jsonEncode({'favorites': []})),
isEmpty,
);
});
test('fehlender Schlüssel ist ein Fehler, keine leere Menge', () {
// Der Router verdrahtet für GET /favorites hart HTTP 200; sechs Handler
// desselben Servers melden Fehler im 200er-Körper. Ein fälschlich
// leeres Ergebnis wäre von einer echten Leerantwort nicht zu
// unterscheiden — wie parseListe und parseUpload wird deshalb geworfen.
expect(
() => MeloCloudService.parseFavoriten(jsonEncode({'status': 'ok'})),
throwsA(isA<CloudException>()),
);
});
test('Server-Fehler im 200er-Körper wird als CloudException gemeldet', () {
expect(
() => MeloCloudService.parseFavoriten(
jsonEncode({'status': 'error', 'error': 'Auth required'})),
throwsA(isA<CloudException>()),
);
});
});
group('endungFuer', () {
test('erkennt die gängigen Formate der Bibliothek', () {
expect(MeloCloudService.endungFuer('audio/mpeg'), '.mp3');
expect(MeloCloudService.endungFuer('audio/mp4'), '.m4a');
expect(MeloCloudService.endungFuer('audio/flac'), '.flac');
expect(MeloCloudService.endungFuer('audio/ogg'), '.ogg');
});
test('ignoriert angehängte Parameter und Groß-/Kleinschreibung', () {
expect(MeloCloudService.endungFuer('Audio/MP4; charset=binary'), '.m4a');
});
test('fällt ohne bekannte Zuordnung auf .mp3 zurück', () {
expect(MeloCloudService.endungFuer(null), '.mp3');
expect(MeloCloudService.endungFuer('application/octet-stream'), '.mp3');
});
});
group('CloudVerlauf', () {
test('meldet die Zeit als ISO-Zeit ohne Bruchteile', () {
final eintrag = CloudVerlauf(
cloudId: 'abc',
gespieltAm: DateTime.utc(2026, 8, 21, 7, 28, 5, 123),
positionSekunden: 42,
);
expect(eintrag.toJson(), {
'song_id': 'abc',
'played_at': '2026-08-21T07:28:05',
'position': 42,
});
});
});
group('setzeFavorit', () {
test('meldet den Wunsch deterministisch, nicht als Umschalten', () async {
Map<String, dynamic>? gesendet;
String? pfad;
final dienst = MeloCloudService(
auth: await _angemeldeteAuth(),
client: MockClient((anfrage) async {
pfad = anfrage.url.path;
gesendet = jsonDecode(anfrage.body) as Map<String, dynamic>;
return http.Response(jsonEncode({'status': 'ok'}), 200);
}),
);
await dienst.setzeFavorit('c5', true);
expect(pfad, endsWith('/favorites/toggle'));
expect(gesendet, {'song_id': 'c5', 'set': true});
});
test('kann einen Favoriten auch ausdrücklich entfernen', () async {
Map<String, dynamic>? gesendet;
final dienst = MeloCloudService(
auth: await _angemeldeteAuth(),
client: MockClient((anfrage) async {
gesendet = jsonDecode(anfrage.body) as Map<String, dynamic>;
return http.Response(jsonEncode({'status': 'ok'}), 200);
}),
);
await dienst.setzeFavorit('c5', false);
expect(gesendet, {'song_id': 'c5', 'set': false});
});
});
}