216 lines
6.5 KiB
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});
|
|
});
|
|
});
|
|
}
|