From b6dd9bca45131a6dd04ad5d4d56bac835fe3c6bc Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Sun, 18 Oct 2026 12:45:41 -0700 Subject: [PATCH] fix(mobile): handle rejected video preview authentication Signed-off-by: Tom Brow --- .../message_content/video_preview.dart | 13 +++- .../channels/message_video_auth_test.dart | 59 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 mobile/test/features/channels/message_video_auth_test.dart diff --git a/mobile/lib/features/channels/message_content/video_preview.dart b/mobile/lib/features/channels/message_content/video_preview.dart index 2da8d37f96..e0dafedff5 100644 --- a/mobile/lib/features/channels/message_content/video_preview.dart +++ b/mobile/lib/features/channels/message_content/video_preview.dart @@ -30,8 +30,17 @@ final videoPreviewFrameLoaderProvider = Provider(( ref, ) { final auth = ref.watch(mediaGetAuthServiceProvider); - return (url) async => - _loadVideoPreviewFrame(url, headers: await auth.headersForAsync(url)); + return (url) async { + final Map headers; + try { + headers = await auth.headersForAsync(url); + } catch (_) { + // Queue rejection or a failed signing worker leaves this preview + // unavailable. Do not attempt an unauthenticated video request. + return null; + } + return _loadVideoPreviewFrame(url, headers: headers); + }; }); class _MessageVideoPreview extends HookConsumerWidget { diff --git a/mobile/test/features/channels/message_video_auth_test.dart b/mobile/test/features/channels/message_video_auth_test.dart new file mode 100644 index 0000000000..5a8c716896 --- /dev/null +++ b/mobile/test/features/channels/message_video_auth_test.dart @@ -0,0 +1,59 @@ +import 'dart:isolate'; + +import 'package:buzz/features/channels/message_content.dart'; +import 'package:buzz/shared/relay/media_auth.dart'; +import 'package:buzz/shared/theme/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class _FailingMediaAuth extends MediaGetAuthService { + _FailingMediaAuth(this.error) + : super(baseUrl: 'https://example.com', nsec: null); + + final Object error; + final requests = []; + + @override + Future> headersForAsync(String url) async { + requests.add(url); + throw error; + } +} + +void main() { + for (final failure in { + 'queue overflow': StateError('Media authentication queue is full'), + 'isolate startup': IsolateSpawnException('worker unavailable'), + }.entries) { + testWidgets('posterless video handles auth ${failure.key}', (tester) async { + const url = 'https://example.com/media/clip.mp4'; + final auth = _FailingMediaAuth(failure.value); + await tester.pumpWidget( + ProviderScope( + overrides: [mediaGetAuthServiceProvider.overrideWithValue(auth)], + child: MaterialApp( + theme: AppTheme.light(), + home: const AppMarkdownTheme( + child: Scaffold( + body: MessageContent( + content: '![video]($url)', + tags: [ + ['imeta', 'url $url', 'm video/mp4'], + ], + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(auth.requests, [url]); + expect(find.text('Video attachment'), findsOneWidget); + expect(tester.takeException(), isNull); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + expect(tester.takeException(), isNull); + }); + } +}