Files
Melo/test/downloads/youtube_search_screen_test.dart
Hermes (Server)andClaude Sonnet 5 d03a4e93a0 Cloud-Account-Kontingent: App liest cloud_remaining, sobald der Server es liefert
BakaAuth bekommt verbleibend/merkeVerbleibend analog zu GastZugang.
YtDownloadService liest cloud_remaining bei Cloud-Zugriff (nicht bei
Gast-Zugriff — Felder bleiben getrennt). UI zeigt "Noch N von 100
heute" für angemeldete Cloud-Accounts. Reine App-Seite, wirkungslos
bis der Server (separates Vorhaben) cloud_remaining liefert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012A2pmbnVNPiHdyf2GW8eLP
2026-08-29 13:22:04 +02:00

291 lines
10 KiB
Dart

import 'dart:convert';
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:melo/downloads/youtube_search_screen.dart';
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';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.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);
}
Future<BakaAuth> _angemeldeteAuth() async {
final auth = BakaAuth(
client: MockClient(
(_) async => http.Response(jsonEncode({'token': 'jwt-abc'}), 200)),
speicher: _MemorySpeicher(),
);
await auth.anmelden('Baka', 'geheim');
return auth;
}
Widget _wrap({
required MeloDb db,
required BakaAuth auth,
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
// _pruefeServerUser) — ohne gespeicherte Zugangsdaten bleibt sie ein
// Gast.
ChangeNotifierProvider<DownloadService>(
create: (_) => DownloadService(
db: db, navidrome: navidrome ?? NavidromeService()),
),
],
child: const MaterialApp(home: Scaffold(body: YoutubeSearchScreen())),
);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
// _pruefeServerUser prüft per NavidromeService.ladeGespeicherteZugangs-
// daten(), ob ein Server-User verbunden ist — ohne diese Antwort bleibt
// der Aufruf im Test unbeantwortet hängen.
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'),
(call) async => null,
);
testWidgets('ohne Anmeldung erscheint der Hinweis auf die Baka-Anmeldung',
(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);
await tester.pumpWidget(_wrap(
db: db, auth: auth, search: search, download: download));
await tester.pumpAndSettle();
expect(find.textContaining('Baka-Server'), findsOneWidget);
expect(find.text('Nach Musikvideos suchen'), findsOneWidget);
await db.close();
});
testWidgets('Server-User sieht keinen Anmelde-Hinweis', (tester) async {
final db = MeloDb(NativeDatabase.memory());
final auth = BakaAuth(
client: MockClient(
(_) async => http.Response(jsonEncode({'token': 'jwt'}), 200)),
speicher: _MemorySpeicher(),
);
final search = YtSearchService(auth: auth);
final download = YtDownloadService(auth: auth);
final nav = NavidromeService()
..setCredentials('https://musik.baka-net.de', 'Dustin', 'geheim');
await tester.pumpWidget(_wrap(
db: db, auth: auth, search: search, download: download,
navidrome: nav));
await tester.pumpAndSettle();
expect(find.textContaining('Baka-Anmeldung'), findsNothing);
await db.close();
});
testWidgets(
'Server-User bekommt bei fehlgeschlagener Auto-Anmeldung wieder '
'den Anmelde-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 nav = NavidromeService()
..setCredentials('https://musik.baka-net.de', 'Dustin', 'falsch');
await tester.pumpWidget(_wrap(
db: db, auth: auth, search: search, download: download,
navidrome: nav));
await tester.pumpAndSettle();
expect(find.textContaining('Baka-Anmeldung'), findsOneWidget);
await db.close();
});
testWidgets('Suche zeigt Titel und Dauer der Treffer', (tester) async {
final db = MeloDb(NativeDatabase.memory());
final auth = await _angemeldeteAuth();
final search = YtSearchService(
auth: auth,
client: MockClient((req) async {
expect(req.url.queryParameters['q'], 'roses');
return http.Response(
jsonEncode([
{
'title': 'Neoni - Roses',
'url': 'https://youtu.be/dQw4w9WgXcQ',
'duration': 187,
},
]),
200);
}),
);
final download = YtDownloadService(auth: auth);
await tester.pumpWidget(_wrap(
db: db, auth: auth, search: search, download: download));
await tester.enterText(find.byType(TextField), 'roses');
await tester.tap(find.text('Suchen'));
await tester.pumpAndSettle();
expect(find.text('Neoni - Roses'), findsOneWidget);
expect(find.text('3:07'), findsOneWidget);
expect(find.byIcon(Icons.download_for_offline_outlined), findsOneWidget);
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
// (auch ganz ohne dieses Widget reproduzierbar) und lässt den Testlauf
// entweder hängen oder an der Pending-Timer-Prüfung scheitern. Aus
// demselben Grund hat auch der bestehende Download-Knopf in
// _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();
});
}