Favoriten-GET gehaertet + deterministisches setzeFavorit
This commit is contained in:
@@ -114,10 +114,22 @@ class MeloCloudService {
|
||||
}
|
||||
|
||||
/// Liest die Favoriten-IDs aus einer Server-Antwort.
|
||||
///
|
||||
/// Ein fehlender `favorites`-Schlüssel ist ein **Fehler, keine leere
|
||||
/// Menge**: der Router verdrahtet für `GET /favorites` hart HTTP 200, und
|
||||
/// mehrere Handler desselben Servers melden Fehler im 200er-Körper. Eine
|
||||
/// fälschlich leere Antwort wäre sonst von einer echten nicht zu
|
||||
/// unterscheiden — genau wie bei [parseListe] und [parseUpload] wird
|
||||
/// deshalb geworfen.
|
||||
@visibleForTesting
|
||||
static List<String> parseFavoriten(String body) {
|
||||
final daten = jsonDecode(body) as Map<String, dynamic>;
|
||||
final liste = daten['favorites'] as List? ?? const [];
|
||||
final fehler = daten['error'] as String?;
|
||||
if (fehler != null) throw CloudException(fehler);
|
||||
final liste = daten['favorites'];
|
||||
if (liste is! List) {
|
||||
throw CloudException('Antwort ohne Favoritenliste');
|
||||
}
|
||||
return [
|
||||
for (final j in liste) (j as Map<String, dynamic>)['id'] as String,
|
||||
];
|
||||
@@ -264,6 +276,23 @@ class MeloCloudService {
|
||||
return parseFavoriten(antwort.body);
|
||||
}
|
||||
|
||||
/// Setzt einen einzelnen Favoriten am Server — additiv oder entfernend,
|
||||
/// aber immer **deterministisch**.
|
||||
///
|
||||
/// Bewusst kein Umschalten: hätte der Server einen abweichenden Stand,
|
||||
/// kehrte ein Toggle den Wunsch des Nutzers um.
|
||||
Future<void> setzeFavorit(String cloudId, bool gesetzt) async {
|
||||
_pruefeAnmeldung();
|
||||
final antwort = await _client
|
||||
.post(
|
||||
Uri.parse('$basisUrl/favorites/toggle'),
|
||||
headers: {..._kopf, 'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'song_id': cloudId, 'set': gesetzt}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
_pruefeStatus(antwort);
|
||||
}
|
||||
|
||||
/// Ersetzt die Favoriten am Server durch [cloudIds].
|
||||
Future<void> setzeFavoriten(List<String> cloudIds) async {
|
||||
_pruefeAnmeldung();
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
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', () {
|
||||
@@ -95,9 +117,30 @@ void main() {
|
||||
expect(MeloCloudService.parseFavoriten(body), ['a', 'b']);
|
||||
});
|
||||
|
||||
test('ohne Favoriten leere Liste', () {
|
||||
expect(MeloCloudService.parseFavoriten(jsonEncode({'status': 'ok'})),
|
||||
isEmpty);
|
||||
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>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,4 +177,39 @@ void main() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user