Favoriten-GET gehaertet + deterministisches setzeFavorit

This commit is contained in:
Hermes (Server)
2026-08-27 10:47:46 +02:00
parent 6250fb78c1
commit 09d24abf4d
2 changed files with 111 additions and 4 deletions
+30 -1
View File
@@ -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();
+81 -3
View File
@@ -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});
});
});
}