fix: retry verification browser launch and failed searches

This commit is contained in:
zarzet committed 2026-09-23 17:26:40 +07:00
1 parent ba44bafa92
commit 39273edd85
4 files changed
+204 -4

No files matched your search

@@ -0,0 +1,137 @@
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 = <bool>[];
messenger.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'launch');
final arguments = Map<String, dynamic>.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();
}
+52
View File
@@ -82,6 +82,48 @@ void main() {
expect(tester.takeException(), isNull);
},
);
testWidgets('failed verification search can retry the same query', (
tester,
) async {
tester.view.physicalSize = const Size(430, 932);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final search = _Search(failFirst: true);
await tester.pumpWidget(
ProviderScope(
overrides: [
settingsProvider.overrideWith(_Settings.new),
extensionProvider.overrideWith(_Extensions.new),
exploreProvider.overrideWith(_Explore.new),
downloadHistoryProvider.overrideWith(_History.new),
recentAccessProvider.overrideWith(_Recent.new),
trackProvider.overrideWith(() => search),
],
child: MaterialApp(
theme: MornyeTheme.build(Brightness.dark),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const _Tabs(),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Open Search'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'Example');
await tester.testTextInput.receiveAction(TextInputAction.search);
await tester.pumpAndSettle();
expect(search._requests, 1);
expect(find.text('Found artist'), findsNothing);
await tester.tap(find.byType(TextField));
await tester.testTextInput.receiveAction(TextInputAction.search);
await tester.pumpAndSettle();
expect(search._requests, 2);
expect(find.text('Found artist'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
class _Tabs extends StatefulWidget {
@@ -185,6 +227,9 @@ class _Recent extends RecentAccessNotifier {
}
class _Search extends TrackNotifier {
_Search({bool failFirst = false}) : _failFirst = failFirst;
final bool _failFirst;
int _requests = 0;
@override
@@ -196,6 +241,13 @@ class _Search extends TrackNotifier {
bool allowVerificationRetry = true,
}) async {
_requests++;
if (_failFirst && _requests == 1) {
state = const TrackState(
hasSearchText: true,
error: 'verification_required',
);
return;
}
state = const TrackState(
hasSearchText: true,
searchExtensionId: 'example',