import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/extension_auth_launcher.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); const channel = MethodChannel('plugins.flutter.io/url_launcher'); const backend = MethodChannel('com.zarz.spotiflac/backend'); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; final uri = Uri.parse('https://example.invalid/verify?state=sample'); tearDown(() { messenger.setMockMethodCallHandler(channel, null); messenger.setMockMethodCallHandler(backend, null); }); for (final inAppFirst in [true, false]) { final browserMode = inAppFirst ? 'in_app_first' : 'external_first'; for (final throwsError in [true, false]) { test( 'browser fallback after launch failure: $browserMode, throws=$throwsError', () async { final modes = []; messenger.setMockMethodCallHandler(channel, (call) async { expect(call.method, 'launch'); final arguments = Map.from(call.arguments as Map); expect(arguments['url'], uri.toString()); modes.add(arguments['useWebView'] as bool); if (modes.length == 1) { if (throwsError) { throw PlatformException(code: 'ACTIVITY_NOT_FOUND'); } return false; } return true; }); expect( await launchExtensionAuthUrl(uri, browserMode: browserMode), isTrue, ); expect(modes, [inAppFirst, !inAppFirst]); }, ); } } test( 'both browser failures return false so manual help can be shown', () async { var launches = 0; messenger.setMockMethodCallHandler(channel, (_) async { launches++; throw PlatformException(code: 'ACTIVITY_NOT_FOUND'); }); expect( await launchExtensionAuthUrl(uri, browserMode: 'in_app_first'), isFalse, ); expect(launches, 2); }, ); test( 'direct search verifies through the fallback browser and retries once', () async { var searches = 0; var pendingLookups = 0; var launches = 0; messenger.setMockMethodCallHandler(backend, (call) async { switch (call.method) { case 'customSearchWithExtension': searches++; if (searches == 1) { throw PlatformException( code: 'ERROR', message: "verification_required: extension 'sample-provider' needs signed-session verification: Error: VERIFY_REQUIRED", ); } return [ { 'id': 'sample-track', 'name': 'Sample track', 'artist_name': 'Artist', }, ]; case 'getExtensionPendingAuth': pendingLookups++; expect(call.arguments, {'extension_id': 'sample-provider'}); return {'auth_url': uri.toString()}; case 'completeExtensionSessionGrant': return true; default: fail('Unexpected backend call: ${call.method}'); } }); messenger.setMockMethodCallHandler(channel, (call) async { launches++; if (launches == 1) throw PlatformException(code: 'ACTIVITY_NOT_FOUND'); unawaited( PlatformBridge.completeExtensionSessionGrant( 'sample-provider', 'test-grant', ), ); return true; }); final container = ProviderContainer( overrides: [settingsProvider.overrideWith(_Settings.new)], ); addTearDown(container.dispose); await container .read(trackProvider.notifier) .customSearch('sample-provider', 'fallback verification'); final state = container.read(trackProvider); expect(state.error, isNull); expect(state.isLoading, isFalse); expect(state.tracks.single.id, 'sample-track'); expect(searches, 2); expect(pendingLookups, 1); expect(launches, 2); }, ); } class _Settings extends SettingsNotifier { @override AppSettings build() => const AppSettings(); }