mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-20 08:42:16 +02:00
fix(download): route verification notifications to pending challenges
This commit is contained in:
@@ -1,9 +1,76 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/models/download_item.dart';
|
||||
import 'package:spotiflac_android/utils/download_error_type.dart';
|
||||
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
|
||||
|
||||
void main() {
|
||||
test('typed provider auth errors override misleading verification text', () {
|
||||
for (final type in [
|
||||
'authentication_error',
|
||||
'PROVIDER_AUTH_FAILED',
|
||||
'request_auth_invalid',
|
||||
'provider_reauth_required',
|
||||
]) {
|
||||
final errorType =
|
||||
downloadErrorTypeFromBackend(type) ??
|
||||
(isExtensionVerificationRequired('Verification required upstream')
|
||||
? DownloadErrorType.verificationRequired
|
||||
: DownloadErrorType.unknown);
|
||||
expect(errorType, DownloadErrorType.unknown, reason: type);
|
||||
}
|
||||
expect(downloadErrorTypeFromBackend(null), isNull);
|
||||
expect(downloadErrorTypeFromBackend('unknown'), isNull);
|
||||
expect(
|
||||
downloadErrorTypeFromBackend('verification_required'),
|
||||
DownloadErrorType.verificationRequired,
|
||||
);
|
||||
});
|
||||
|
||||
test('provider auth and HTTP status errors do not imply verification', () {
|
||||
for (final message in [
|
||||
'Provider unauthorized',
|
||||
'HTTP 401 for /download',
|
||||
'HTTP status 428: precondition required',
|
||||
'PROVIDER_AUTH_FAILED: unauthorized',
|
||||
]) {
|
||||
expect(
|
||||
isExtensionVerificationRequired(message),
|
||||
isFalse,
|
||||
reason: message,
|
||||
);
|
||||
}
|
||||
for (final message in [
|
||||
'verification_required: canonical gateway challenge',
|
||||
'VERIFY_REQUIRED',
|
||||
'signed session is not authenticated',
|
||||
'signed session expired',
|
||||
]) {
|
||||
expect(isExtensionVerificationRequired(message), isTrue, reason: message);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'does not open a verification flow for a provider authentication error',
|
||||
() async {
|
||||
var verificationCalls = 0;
|
||||
await expectLater(
|
||||
runExtensionOperationWithVerificationRetry<void>(
|
||||
extensionId: 'provider-a',
|
||||
browserMode: 'in_app_first',
|
||||
operation: () async => throw StateError('Provider unauthorized'),
|
||||
verify: () async {
|
||||
verificationCalls++;
|
||||
return true;
|
||||
},
|
||||
),
|
||||
throwsStateError,
|
||||
);
|
||||
expect(verificationCalls, 0);
|
||||
},
|
||||
);
|
||||
|
||||
test('verification challenge expires after three minutes', () {
|
||||
expect(extensionVerificationGrantTimeout, const Duration(minutes: 3));
|
||||
});
|
||||
@@ -11,20 +78,20 @@ void main() {
|
||||
test('extracts the extension that raised a verification challenge', () {
|
||||
expect(
|
||||
extensionIdFromVerificationError(
|
||||
"verification_required: extension 'tidal-web' needs verification",
|
||||
const ['amazon-web', 'tidal-web'],
|
||||
"verification_required: extension 'provider-b' needs verification",
|
||||
const ['provider-a', 'provider-b'],
|
||||
),
|
||||
'tidal-web',
|
||||
'provider-b',
|
||||
);
|
||||
});
|
||||
|
||||
test('prefers the longest known extension id in legacy errors', () {
|
||||
expect(
|
||||
extensionIdFromVerificationError(
|
||||
'qobuz-web verification_required',
|
||||
const ['qobuz', 'qobuz-web'],
|
||||
'sample-provider verification_required',
|
||||
const ['sample', 'sample-provider'],
|
||||
),
|
||||
'qobuz-web',
|
||||
'sample-provider',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -35,7 +102,7 @@ void main() {
|
||||
var verificationCalls = 0;
|
||||
|
||||
final result = await runExtensionOperationWithVerificationRetry(
|
||||
extensionId: 'qobuz-web',
|
||||
extensionId: 'provider-a',
|
||||
browserMode: 'in_app_first',
|
||||
operation: () async {
|
||||
operationCalls++;
|
||||
@@ -59,7 +126,7 @@ void main() {
|
||||
|
||||
await expectLater(
|
||||
runExtensionOperationWithVerificationRetry(
|
||||
extensionId: 'qobuz-web',
|
||||
extensionId: 'provider-a',
|
||||
browserMode: 'in_app_first',
|
||||
operation: () async {
|
||||
operationCalls++;
|
||||
@@ -77,7 +144,7 @@ void main() {
|
||||
final foreground = Completer<void>();
|
||||
final cancellation = Completer<void>();
|
||||
final result = openVerificationAndAwaitGrant(
|
||||
'tidal-web',
|
||||
'provider-a',
|
||||
browserMode: 'in_app_first',
|
||||
awaitForeground: (_) => foreground.future,
|
||||
cancellationSignal: cancellation.future,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
import 'package:spotiflac_android/services/app_navigation_service.dart';
|
||||
import 'package:spotiflac_android/utils/extension_auth_launcher.dart';
|
||||
|
||||
void main() {
|
||||
const channel = MethodChannel('com.zarz.spotiflac/backend');
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
|
||||
Future<void> showApp(WidgetTester tester) => tester.pumpWidget(
|
||||
MaterialApp(
|
||||
navigatorKey: AppNavigationService.rootNavigatorKey,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const Scaffold(body: Text('Library')),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('missing pending challenge explains why no page opened', (
|
||||
tester,
|
||||
) async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
expect(call.method, 'getExtensionPendingAuth');
|
||||
return null;
|
||||
});
|
||||
await showApp(tester);
|
||||
expect(await openPendingExtensionVerification('provider-a'), isFalse);
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.text(
|
||||
'No verification page is available for provider-a. '
|
||||
'Retry the download to request a new challenge.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('cancelled pending lookup does not show a failure message', (
|
||||
tester,
|
||||
) async {
|
||||
final pending = Completer<Object?>();
|
||||
final cancellation = Completer<void>();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (_) => pending.future);
|
||||
await showApp(tester);
|
||||
final result = openPendingExtensionVerification(
|
||||
'provider-a',
|
||||
cancellationSignal: cancellation.future,
|
||||
);
|
||||
cancellation.complete();
|
||||
expect(await result, isFalse);
|
||||
pending.complete();
|
||||
await tester.pump();
|
||||
expect(find.byType(SnackBar), findsNothing);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/services/notification_service.dart';
|
||||
import 'package:spotiflac_android/services/verification_notification.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -10,6 +11,11 @@ void main() {
|
||||
|
||||
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
|
||||
final methodCalls = <MethodCall>[];
|
||||
const coldTarget = VerificationNotification(
|
||||
extensionId: 'provider-a',
|
||||
itemId: 'cold-source-item',
|
||||
tapId: 'dart:cold-tap',
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
@@ -17,6 +23,16 @@ void main() {
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
methodCalls.add(call);
|
||||
if (call.method == 'initialize') return true;
|
||||
if (call.method == 'getNotificationAppLaunchDetails') {
|
||||
return {
|
||||
'notificationLaunchedApp': true,
|
||||
'notificationResponse': {
|
||||
'notificationId': 4,
|
||||
'notificationResponseType': 0,
|
||||
'payload': coldTarget.encode(),
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
@@ -30,7 +46,10 @@ void main() {
|
||||
|
||||
test('verification uses a distinct audible Android alert channel', () async {
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.showVerificationRequired();
|
||||
await notificationService.showVerificationRequired(
|
||||
extensionId: 'provider-b',
|
||||
itemId: 'source-item',
|
||||
);
|
||||
|
||||
final alertChannelCall = methodCalls.firstWhere(
|
||||
(call) =>
|
||||
@@ -58,6 +77,33 @@ void main() {
|
||||
expect(androidDetails['importance'], Importance.defaultImportance.value);
|
||||
expect(androidDetails['playSound'], isTrue);
|
||||
expect(androidDetails['enableVibration'], isTrue);
|
||||
final target = VerificationNotification.parse(showArguments['payload']);
|
||||
expect(target?.extensionId, 'provider-b');
|
||||
expect(target?.itemId, 'source-item');
|
||||
|
||||
final tapped = <VerificationNotification>[];
|
||||
notificationService.verificationNotifications.setHandler((target) async {
|
||||
tapped.add(target);
|
||||
});
|
||||
expect(tapped.single.extensionId, 'provider-a');
|
||||
expect(tapped.single.itemId, 'cold-source-item');
|
||||
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.handlePlatformMessage(
|
||||
channel.name,
|
||||
const StandardMethodCodec().encodeMethodCall(
|
||||
MethodCall('didReceiveNotificationResponse', {
|
||||
'notificationResponseType': 0,
|
||||
'id': 4,
|
||||
'payload': showArguments['payload'],
|
||||
}),
|
||||
),
|
||||
(_) {},
|
||||
);
|
||||
expect(tapped.map((target) => target.extensionId), [
|
||||
'provider-a',
|
||||
'provider-b',
|
||||
]);
|
||||
notificationService.verificationNotifications.setHandler(null);
|
||||
|
||||
await notificationService.cancelVerificationRequired();
|
||||
expect(
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/services/verification_notification.dart';
|
||||
|
||||
void main() {
|
||||
const target = VerificationNotification(
|
||||
extensionId: 'provider-a',
|
||||
itemId: 'source-track',
|
||||
tapId: 'native:tap-1',
|
||||
);
|
||||
|
||||
test(
|
||||
'cold tap waits for a ready handler and keeps fallback ownership',
|
||||
() async {
|
||||
final router = VerificationNotificationRouter();
|
||||
final seen = <VerificationNotification>[];
|
||||
router.receive(target.encode());
|
||||
router.receive(target.encode());
|
||||
expect(seen, isEmpty);
|
||||
router.setHandler((value) async => seen.add(value));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(seen.single.extensionId, 'provider-a');
|
||||
expect(seen.single.itemId, 'source-track');
|
||||
router.receive(target.encode());
|
||||
expect(seen, hasLength(1));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'duplicate active tap is ignored without blocking another provider',
|
||||
() async {
|
||||
final router = VerificationNotificationRouter();
|
||||
final wait = Completer<void>();
|
||||
final seen = <String>[];
|
||||
router.setHandler((value) async {
|
||||
seen.add(value.extensionId);
|
||||
await wait.future;
|
||||
});
|
||||
router.receive(target.encode());
|
||||
router.receive(target.encode());
|
||||
router.receive(
|
||||
const VerificationNotification(
|
||||
extensionId: 'provider-b',
|
||||
itemId: 'another-track',
|
||||
tapId: 'dart:tap-2',
|
||||
).encode(),
|
||||
);
|
||||
expect(seen, ['provider-a', 'provider-b']);
|
||||
wait.complete();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
},
|
||||
);
|
||||
|
||||
test('unrelated and incomplete notifications cannot guess an extension', () {
|
||||
for (final payload in [
|
||||
null,
|
||||
'',
|
||||
'not json',
|
||||
'{}',
|
||||
{'kind': 'extension_verification', 'item_id': 'track', 'tap_id': 'tap'},
|
||||
{
|
||||
'kind': 'other',
|
||||
'extension_id': 'provider-a',
|
||||
'item_id': 'track',
|
||||
'tap_id': 'tap',
|
||||
},
|
||||
]) {
|
||||
expect(VerificationNotification.parse(payload), isNull);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user