Merge branch 'feature/youtube-gast-zugang'

# Conflicts:
#	CHANGELOG.md
#	test/downloads/online_screen_test.dart
This commit is contained in:
Hermes (Server)
2026-08-30 10:34:58 +02:00
19 changed files with 1138 additions and 36 deletions
+108 -1
View File
@@ -11,6 +11,7 @@ import 'package:melo/library/category_service.dart';
import 'package:melo/library/database.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/download_service.dart';
import 'package:melo/services/gast_zugang.dart';
import 'package:melo/services/navidrome_service.dart';
import 'package:melo/services/yt_download_service.dart';
import 'package:provider/provider.dart';
@@ -29,7 +30,9 @@ class _MemorySpeicher implements TokenSpeicher {
Widget _wrap(BakaAuth auth,
{required MeloDb db,
required CategoryService categories,
NavidromeService? navidrome}) {
NavidromeService? navidrome,
GastZugang? gast}) {
final gastZugang = gast ?? GastZugang();
return MultiProvider(
providers: [
Provider<MeloDb>.value(value: db),
@@ -38,9 +41,11 @@ Widget _wrap(BakaAuth auth,
// Aufräum-Timer von drift im Test-Rahmen hängen.
ChangeNotifierProvider<CategoryService>.value(value: categories),
ChangeNotifierProvider<BakaAuth>.value(value: auth),
ChangeNotifierProvider<GastZugang>.value(value: gastZugang),
ChangeNotifierProvider<YtDownloadService>(
create: (_) => YtDownloadService(
auth: auth,
gast: gastZugang,
client: MockClient((_) async => http.Response('', 500)),
),
),
@@ -301,6 +306,21 @@ void main() {
expect(auth.istAngemeldet, isTrue);
});
testWidgets(
'Angemeldeter Cloud-Account sieht das Kontingent nach einem Download',
(tester) async {
final auth = await _angemeldet();
await tester.pumpWidget(_wrap(auth, db: db, categories: categories));
await tester.pumpAndSettle();
expect(find.textContaining('von 100 heute'), findsNothing);
auth.merkeVerbleibend(63);
await tester.pump();
expect(find.text('Noch 63 von 100 heute'), findsOneWidget);
});
testWidgets(
'Server-User meldet sich im Hintergrund mit denselben Zugangsdaten '
'beim Baka-Konto an', (tester) async {
@@ -323,4 +343,91 @@ void main() {
expect(gesendeterBenutzer, 'Tinker');
expect(auth.istAngemeldet, isTrue);
});
testWidgets('Gast sieht zwei Buttons: Anmelden und Als Gast fortfahren',
(tester) async {
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
await tester.pumpWidget(_wrap(auth, db: db, categories: categories));
await tester.pumpAndSettle();
expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget);
expect(
find.text('Als Gast fortfahren (5 Downloads/Tag)'), findsOneWidget);
expect(find.text('YouTube-Adresse'), findsNothing);
});
testWidgets(
'Als Gast fortfahren holt einen Token und schaltet das Adressfeld frei',
(tester) async {
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
final gast = GastZugang(
client: MockClient((_) async =>
http.Response(jsonEncode({'guest_token': 'g-1'}), 200)),
speicher: _MemorySpeicher(),
);
await tester.pumpWidget(
_wrap(auth, db: db, categories: categories, gast: gast));
await tester.pumpAndSettle();
await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)'));
await tester.pumpAndSettle();
expect(find.text('YouTube-Adresse'), findsOneWidget);
expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget);
expect(find.text('YouTube-Cookies des Servers verwenden'), findsNothing);
});
testWidgets('Fehler beim Gast-Token wird angezeigt', (tester) async {
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher(),
);
await tester.pumpWidget(
_wrap(auth, db: db, categories: categories, gast: gast));
await tester.pumpAndSettle();
await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)'));
await tester.pumpAndSettle();
expect(find.text('Gast-Zugang fehlgeschlagen (500)'), findsOneWidget);
expect(find.text('YouTube-Adresse'), findsNothing);
});
testWidgets(
'Anmelden im Gast-Modus verwirft den Gast-Token und zeigt wieder die '
'Wahl', (tester) async {
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
final gast = GastZugang(
client: MockClient((_) async =>
http.Response(jsonEncode({'guest_token': 'g-1'}), 200)),
speicher: _MemorySpeicher(),
);
await tester.pumpWidget(
_wrap(auth, db: db, categories: categories, gast: gast));
await tester.pumpAndSettle();
await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)'));
await tester.pumpAndSettle();
expect(find.text('YouTube-Adresse'), findsOneWidget);
await tester.tap(find.text('Anmelden'));
await tester.pumpAndSettle();
expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget);
expect(find.text('YouTube-Adresse'), findsNothing);
expect(gast.hatToken, isFalse);
});
}
+106 -1
View File
@@ -11,6 +11,7 @@ import 'package:melo/library/database.dart';
import 'package:melo/library/library_service.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/download_service.dart';
import 'package:melo/services/gast_zugang.dart';
import 'package:melo/services/navidrome_service.dart';
import 'package:melo/services/yt_download_service.dart';
import 'package:melo/services/yt_search_service.dart';
@@ -43,12 +44,14 @@ Widget _wrap({
required YtSearchService search,
required YtDownloadService download,
NavidromeService? navidrome,
GastZugang? gast,
}) {
return MultiProvider(
providers: [
Provider<MeloDb>.value(value: db),
ChangeNotifierProvider<LibraryService>.value(value: LibraryService(db)),
ChangeNotifierProvider<BakaAuth>.value(value: auth),
ChangeNotifierProvider<GastZugang>.value(value: gast ?? GastZugang()),
ChangeNotifierProvider<YtSearchService>.value(value: search),
ChangeNotifierProvider<YtDownloadService>.value(value: download),
// Nur für die Server-User-Prüfung gebraucht (siehe
@@ -89,7 +92,7 @@ void main() {
db: db, auth: auth, search: search, download: download));
await tester.pumpAndSettle();
expect(find.textContaining('Baka-Anmeldung'), findsOneWidget);
expect(find.textContaining('Baka-Server'), findsOneWidget);
expect(find.text('Nach Musikvideos suchen'), findsOneWidget);
await db.close();
@@ -173,6 +176,26 @@ void main() {
await db.close();
});
testWidgets('Angemeldeter Cloud-Account sieht das Kontingent nach Download',
(tester) async {
final db = MeloDb(NativeDatabase.memory());
final auth = await _angemeldeteAuth();
final search = YtSearchService(auth: auth);
final download = YtDownloadService(auth: auth);
await tester.pumpWidget(_wrap(
db: db, auth: auth, search: search, download: download));
await tester.pumpAndSettle();
expect(find.textContaining('von 100 heute'), findsNothing);
auth.merkeVerbleibend(98);
await tester.pump();
expect(find.text('Noch 98 von 100 heute'), findsOneWidget);
await db.close();
});
// Kein Widget-Test für den Download-Knopf selbst: YtDownloadService.
// herunterladen() kombiniert Future.any mit .timeout() — dessen Timer
// bleibt unter Flutters fake_async-Testbindung immer als "pending" stehen
@@ -182,4 +205,86 @@ void main() {
// _YouTubeBereich (downloads_screen.dart) keinen eigenen Widget-Test —
// die Download-Logik selbst ist über yt_download_test.dart ausführlich
// abgedeckt.
testWidgets('Gast sieht den Button "Als Gast fortfahren"', (tester) async {
final db = MeloDb(NativeDatabase.memory());
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher(),
);
final search = YtSearchService(auth: auth, gast: gast);
final download = YtDownloadService(auth: auth, gast: gast);
await tester.pumpWidget(_wrap(
db: db, auth: auth, search: search, download: download, gast: gast));
await tester.pumpAndSettle();
expect(
find.text('Als Gast fortfahren (5 Downloads/Tag)'), findsOneWidget);
await db.close();
});
testWidgets(
'Als Gast fortfahren blendet den Baka-Hinweis aus und zeigt den Zähler',
(tester) async {
final db = MeloDb(NativeDatabase.memory());
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
final gast = GastZugang(
client: MockClient((_) async =>
http.Response(jsonEncode({'guest_token': 'g-1'}), 200)),
speicher: _MemorySpeicher(),
);
final search = YtSearchService(auth: auth, gast: gast);
final download = YtDownloadService(auth: auth, gast: gast);
await tester.pumpWidget(_wrap(
db: db, auth: auth, search: search, download: download, gast: gast));
await tester.pumpAndSettle();
await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)'));
await tester.pumpAndSettle();
expect(find.textContaining('Baka-Anmeldung'), findsNothing);
expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget);
await db.close();
});
testWidgets(
'Anmelden im Gast-Modus verwirft den Gast-Token und zeigt wieder den '
'Hinweis', (tester) async {
final db = MeloDb(NativeDatabase.memory());
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
final search = YtSearchService(auth: auth);
final download = YtDownloadService(auth: auth);
final gast = GastZugang(
client: MockClient((_) async =>
http.Response(jsonEncode({'guest_token': 'g-1'}), 200)),
speicher: _MemorySpeicher(),
);
await tester.pumpWidget(_wrap(
db: db, auth: auth, search: search, download: download, gast: gast));
await tester.pumpAndSettle();
await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)'));
await tester.pumpAndSettle();
expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget);
await tester.tap(find.text('Anmelden'));
await tester.pumpAndSettle();
expect(find.textContaining('Baka-Server'), findsOneWidget);
expect(gast.hatToken, isFalse);
await db.close();
});
}
+6 -2
View File
@@ -10,6 +10,7 @@ import 'package:melo/player/audio_handler.dart';
import 'package:melo/player/player_expansion_controller.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/download_service.dart';
import 'package:melo/services/gast_zugang.dart';
import 'package:melo/services/melo_cloud_service.dart';
import 'package:melo/services/navidrome_service.dart';
import 'package:melo/services/offline_mode.dart';
@@ -52,6 +53,7 @@ void main() {
late YtSearchService ytSearch;
late DownloadService downloads;
late SyncService sync;
late GastZugang gast;
MeloAudioHandler? handler;
PlayerExpansionController? expansion;
@@ -64,8 +66,9 @@ void main() {
auth = BakaAuth(speicher: _MemorySpeicher());
einstellungen = AppSettings();
offline = OfflineMode();
ytDownload = YtDownloadService(auth: auth);
ytSearch = YtSearchService(auth: auth);
gast = GastZugang();
ytDownload = YtDownloadService(auth: auth, gast: gast);
ytSearch = YtSearchService(auth: auth, gast: gast);
downloads = DownloadService(db: db, navidrome: NavidromeService());
sync = SyncService(db: db, cloud: MeloCloudService(auth: auth));
});
@@ -101,6 +104,7 @@ void main() {
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
ChangeNotifierProvider<OfflineMode>.value(value: offline),
ChangeNotifierProvider<BakaAuth>.value(value: auth),
ChangeNotifierProvider<GastZugang>.value(value: gast),
ChangeNotifierProvider<YtDownloadService>.value(value: ytDownload),
ChangeNotifierProvider<YtSearchService>.value(value: ytSearch),
ChangeNotifierProvider<DownloadService>.value(value: downloads),
+3
View File
@@ -10,6 +10,7 @@ import 'package:melo/player/audio_handler.dart';
import 'package:melo/player/player_expansion_controller.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/download_service.dart';
import 'package:melo/services/gast_zugang.dart';
import 'package:melo/services/melo_cloud_service.dart';
import 'package:melo/services/navidrome_service.dart';
import 'package:melo/services/offline_mode.dart';
@@ -52,6 +53,7 @@ void main() {
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
ChangeNotifierProvider<GastZugang>(create: (_) => GastZugang()),
ChangeNotifierProvider<YtDownloadService>(
create: (_) => YtDownloadService(auth: auth)),
ChangeNotifierProvider<YtSearchService>(
@@ -97,6 +99,7 @@ void main() {
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
ChangeNotifierProvider<GastZugang>(create: (_) => GastZugang()),
ChangeNotifierProvider<YtDownloadService>(
create: (_) => YtDownloadService(auth: auth)),
ChangeNotifierProvider<YtSearchService>(
@@ -11,6 +11,7 @@ import 'package:melo/player/mini_player.dart';
import 'package:melo/player/player_expansion_controller.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/download_service.dart';
import 'package:melo/services/gast_zugang.dart';
import 'package:melo/services/melo_cloud_service.dart';
import 'package:melo/services/navidrome_service.dart';
import 'package:melo/services/offline_mode.dart';
@@ -56,6 +57,7 @@ void main() {
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
ChangeNotifierProvider<GastZugang>(create: (_) => GastZugang()),
ChangeNotifierProvider<YtDownloadService>(
create: (_) => YtDownloadService(auth: auth)),
ChangeNotifierProvider<YtSearchService>(
@@ -125,6 +127,7 @@ void main() {
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
ChangeNotifierProvider<GastZugang>(create: (_) => GastZugang()),
ChangeNotifierProvider<YtDownloadService>(
create: (_) => YtDownloadService(auth: auth)),
ChangeNotifierProvider<YtSearchService>(
@@ -14,6 +14,7 @@ import 'package:melo/player/player_expansion_controller.dart';
import 'package:melo/playlists/playlist_detail_screen.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/download_service.dart';
import 'package:melo/services/gast_zugang.dart';
import 'package:melo/services/melo_cloud_service.dart';
import 'package:melo/services/navidrome_service.dart';
import 'package:melo/services/offline_mode.dart';
@@ -98,6 +99,7 @@ void main() {
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
ChangeNotifierProvider<GastZugang>(create: (_) => GastZugang()),
ChangeNotifierProvider<YtDownloadService>(
create: (_) => YtDownloadService(auth: auth)),
ChangeNotifierProvider<YtSearchService>(
+28
View File
@@ -167,6 +167,34 @@ void main() {
expect(auth.istAngemeldet, isFalse);
});
test('merkeVerbleibend übernimmt den Kontingent-Stand und meldet Listener',
() async {
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
var benachrichtigt = 0;
auth.addListener(() => benachrichtigt++);
auth.merkeVerbleibend(37);
expect(auth.verbleibend, 37);
expect(benachrichtigt, 1);
});
test('Abmelden setzt auch den Kontingent-Stand zurück', () async {
final speicher = _MemorySpeicher();
final auth = BakaAuth(
client: MockClient(
(_) async => http.Response(jsonEncode({'token': 'jwt-abc'}), 200)),
speicher: speicher);
await auth.anmelden('Baka', 'geheim');
auth.merkeVerbleibend(42);
await auth.abmelden();
expect(auth.verbleibend, isNull);
});
test('Abmelden löscht Token und Benutzer aus dem Speicher', () async {
final speicher = _MemorySpeicher();
final auth = BakaAuth(
+120
View File
@@ -0,0 +1,120 @@
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/gast_zugang.dart';
class _MemorySpeicher implements TokenSpeicher {
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);
}
void main() {
test('Ohne Token ist gastHeader leer', () {
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher(),
);
expect(gast.hatToken, isFalse);
expect(gast.gastHeader, isEmpty);
});
test('holeToken() speichert den Token bei Erfolg', () async {
final speicher = _MemorySpeicher();
final gast = GastZugang(
client: MockClient(
(_) async => http.Response(jsonEncode({'guest_token': 'g-1'}), 200)),
speicher: speicher,
);
final fehler = await gast.holeToken();
expect(fehler, isNull);
expect(gast.hatToken, isTrue);
expect(gast.gastHeader, {'X-Guest-Token': 'g-1'});
expect(speicher.werte['guest_token'], 'g-1');
});
test('holeToken() meldet einen Fehler bei einer toten Verbindung',
() async {
final gast = GastZugang(
client: MockClient((_) async => throw Exception('down')),
speicher: _MemorySpeicher(),
);
final fehler = await gast.holeToken();
expect(fehler, 'Proxy nicht erreichbar');
expect(gast.hatToken, isFalse);
});
test('holeToken() meldet einen Fehler bei einem Server-Fehlerstatus',
() async {
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher(),
);
final fehler = await gast.holeToken();
expect(fehler, 'Gast-Zugang fehlgeschlagen (500)');
expect(gast.hatToken, isFalse);
});
test('holeToken() meldet einen Fehler bei unlesbarer Antwort', () async {
final gast = GastZugang(
client: MockClient((_) async => http.Response('nicht json', 200)),
speicher: _MemorySpeicher(),
);
final fehler = await gast.holeToken();
expect(fehler, 'Proxy antwortet unverständlich (200)');
expect(gast.hatToken, isFalse);
});
test('holeToken() meldet einen Fehler ohne guest_token-Feld', () async {
final gast = GastZugang(
client: MockClient((_) async => http.Response(jsonEncode({}), 200)),
speicher: _MemorySpeicher(),
);
final fehler = await gast.holeToken();
expect(fehler, 'Proxy hat keinen Gast-Token geliefert');
expect(gast.hatToken, isFalse);
});
test('laden() stellt einen gespeicherten Gast-Token wieder her', () async {
final speicher = _MemorySpeicher();
speicher.werte['guest_token'] = 'g-alt';
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: speicher,
);
await gast.laden();
expect(gast.hatToken, isTrue);
expect(gast.gastHeader, {'X-Guest-Token': 'g-alt'});
});
test('merkeVerbleibend übernimmt den zuletzt gemeldeten Stand', () {
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher(),
);
gast.merkeVerbleibend(3);
expect(gast.verbleibend, 3);
});
}
+168
View File
@@ -6,6 +6,7 @@ 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/gast_zugang.dart';
import 'package:melo/services/yt_download_service.dart';
class _MemorySpeicher implements TokenSpeicher {
@@ -284,4 +285,171 @@ void main() {
expect((jsonDecode(auftrag!) as Map)['cookies'], isFalse);
});
group('Gast-Zugang', () {
test('Mit Gast-Token statt Anmeldung wird der Proxy trotzdem gefragt',
() async {
final speicher = _MemorySpeicher();
speicher.werte['guest_token'] = 'gast-xyz';
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: speicher,
);
await gast.laden();
final dienst = YtDownloadService(
auth: BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher()),
gast: gast,
client: MockClient((req) async {
if (req.url.path == '/api/yt-dl') {
expect(req.headers['X-Guest-Token'], 'gast-xyz');
expect(req.headers.containsKey('Authorization'), isFalse);
return http.Response(
jsonEncode({
'titel': 'Gast-Lied',
'dauer': 30,
'mp3_url': '/api/dl/g.mp3',
'guest_remaining': 4,
}),
200);
}
return http.Response.bytes([1], 200);
}),
);
final ergebnis = await dienst.herunterladen('https://youtu.be/abc',
zielOrdner: ziel.path);
expect(ergebnis, isNotNull);
expect(gast.verbleibend, 4);
});
test('Ohne Anmeldung UND ohne Gast-Token wird gar nicht erst gefragt',
() async {
var aufrufe = 0;
final dienst = YtDownloadService(
auth: BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher()),
client: MockClient((_) async {
aufrufe++;
return http.Response('', 200);
}),
);
final ergebnis = await dienst.herunterladen('https://youtu.be/abc',
zielOrdner: ziel.path);
expect(ergebnis, isNull);
expect(dienst.fehler, 'Bitte zuerst beim Baka-Konto anmelden');
expect(aufrufe, 0);
});
test('401 bei Gast-Zugang holt automatisch einen neuen Gast-Token',
() async {
var tokenAufrufe = 0;
final speicher = _MemorySpeicher();
speicher.werte['guest_token'] = 'alter-token';
final gast = GastZugang(
client: MockClient((_) async {
tokenAufrufe++;
return http.Response(
jsonEncode({'guest_token': 'neuer-token'}), 200);
}),
speicher: speicher,
);
await gast.laden();
final dienst = YtDownloadService(
auth: BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher()),
gast: gast,
client: MockClient((_) async => http.Response('Unauthorized', 401)),
);
final ergebnis = await dienst.herunterladen('https://youtu.be/abc',
zielOrdner: ziel.path);
// Das Token-Nachholen läuft unawaited im Hintergrund.
await Future<void>.delayed(Duration.zero);
expect(ergebnis, isNull);
expect(dienst.fehler, 'Gast-Zugang abgelaufen — bitte erneut versuchen');
expect(tokenAufrufe, 1);
});
});
group('Cloud-Kontingent', () {
test('cloud_remaining aus der Erfolgsantwort landet in auth.verbleibend',
() async {
final auth = await _angemeldeteAuth();
final dienst = YtDownloadService(
auth: auth,
client: MockClient((req) async {
if (req.url.path == '/api/yt-dl') {
return http.Response(
jsonEncode({
'titel': 'Cloud-Lied',
'dauer': 30,
'mp3_url': '/api/dl/c.mp3',
'cloud_remaining': 99,
}),
200);
}
return http.Response.bytes([1], 200);
}),
);
final ergebnis = await dienst.herunterladen('https://youtu.be/abc',
zielOrdner: ziel.path);
expect(ergebnis, isNotNull);
expect(auth.verbleibend, 99);
});
test(
'Bei Gast-Zugriff wird cloud_remaining nicht gelesen, selbst wenn es '
'im Body steht', () async {
final speicher = _MemorySpeicher();
speicher.werte['guest_token'] = 'gast-xyz';
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: speicher,
);
await gast.laden();
final auth = BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher());
final dienst = YtDownloadService(
auth: auth,
gast: gast,
client: MockClient((req) async {
if (req.url.path == '/api/yt-dl') {
return http.Response(
jsonEncode({
'titel': 'Gast-Lied',
'dauer': 30,
'mp3_url': '/api/dl/g.mp3',
'guest_remaining': 4,
// Sollte serverseitig nie gleichzeitig vorkommen, aber
// falls doch: darf nicht in auth.verbleibend landen.
'cloud_remaining': 77,
}),
200);
}
return http.Response.bytes([1], 200);
}),
);
final ergebnis = await dienst.herunterladen('https://youtu.be/abc',
zielOrdner: ziel.path);
expect(ergebnis, isNotNull);
expect(gast.verbleibend, 4);
expect(auth.verbleibend, isNull);
});
});
}
+57
View File
@@ -5,6 +5,7 @@ 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/gast_zugang.dart';
import 'package:melo/services/yt_search_service.dart';
class _MemorySpeicher implements TokenSpeicher {
@@ -200,5 +201,61 @@ void main() {
await dienst.suchen('zweite suche');
expect(dienst.treffer.single.title, 'Zweites');
});
test('Mit Gast-Token statt Anmeldung wird trotzdem gesucht', () async {
final speicher = _MemorySpeicher();
speicher.werte['guest_token'] = 'gast-xyz';
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: speicher,
);
await gast.laden();
final dienst = YtSearchService(
auth: BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher()),
gast: gast,
client: MockClient((req) async {
expect(req.headers['X-Guest-Token'], 'gast-xyz');
expect(req.headers.containsKey('Authorization'), isFalse);
return http.Response('[]', 200);
}),
);
await dienst.suchen('roses');
expect(dienst.fehler, isNull);
});
test('401 bei Gast-Zugang holt automatisch einen neuen Gast-Token',
() async {
var tokenAufrufe = 0;
final speicher = _MemorySpeicher();
speicher.werte['guest_token'] = 'alter-token';
final gast = GastZugang(
client: MockClient((_) async {
tokenAufrufe++;
return http.Response(
jsonEncode({'guest_token': 'neuer-token'}), 200);
}),
speicher: speicher,
);
await gast.laden();
final dienst = YtSearchService(
auth: BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher()),
gast: gast,
client: MockClient((_) async => http.Response('Unauthorized', 401)),
);
await dienst.suchen('roses');
await Future<void>.delayed(Duration.zero);
expect(dienst.fehler, 'Gast-Zugang abgelaufen — bitte erneut versuchen');
expect(tokenAufrufe, 1);
});
});
}