mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
fix: retry verification browser launch and failed searches
This commit is contained in:
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();
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user