mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: rm integration tests

-376
-252
integration_test/auth_token_recovery_flow_test.dart
··· 1 - import 'dart:async'; 2 - 3 - import 'package:atproto_core/atproto_core.dart'; 4 - import 'package:bloc_test/bloc_test.dart'; 5 - import 'package:bluesky/app_bsky_actor_defs.dart'; 6 - import 'package:bluesky/app_bsky_feed_defs.dart'; 7 - import 'package:drift/native.dart'; 8 - import 'package:flutter/material.dart'; 9 - import 'package:flutter_bloc/flutter_bloc.dart'; 10 - import 'package:flutter_test/flutter_test.dart'; 11 - import 'package:integration_test/integration_test.dart'; 12 - import 'package:lazurite/core/database/app_database.dart'; 13 - import 'package:lazurite/core/theme/app_theme.dart'; 14 - import 'package:lazurite/core/theme/feed_layout.dart'; 15 - import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 16 - import 'package:lazurite/features/auth/data/models/auth_models.dart'; 17 - import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; 18 - import 'package:lazurite/features/feed/cubit/feed_preferences_cubit.dart'; 19 - import 'package:lazurite/features/feed/cubit/post_action_cache.dart'; 20 - import 'package:lazurite/features/feed/cubit/saved_posts_cubit.dart'; 21 - import 'package:lazurite/features/feed/data/feed_repository.dart'; 22 - import 'package:lazurite/features/feed/data/post_action_repository.dart'; 23 - import 'package:lazurite/features/feed/presentation/home_feed_screen.dart'; 24 - import 'package:lazurite/features/feed/presentation/widgets/post_card_with_actions.dart'; 25 - import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; 26 - import 'package:lazurite/features/settings/bloc/settings_state.dart'; 27 - import 'package:mocktail/mocktail.dart'; 28 - 29 - class MockSettingsCubit extends MockCubit<SettingsState> implements SettingsCubit {} 30 - 31 - class MockFeedPreferencesCubit extends MockCubit<FeedPreferencesState> implements FeedPreferencesCubit {} 32 - 33 - class MockConnectivityCubit extends MockCubit<ConnectivityState> implements ConnectivityCubit {} 34 - 35 - class MockAuthBloc extends MockBloc<AuthEvent, AuthState> implements AuthBloc {} 36 - 37 - class MockSavedPostsCubit extends MockCubit<SavedPostsState> implements SavedPostsCubit {} 38 - 39 - class MockPostActionRepository extends Mock implements PostActionRepository {} 40 - 41 - class _FakeFeedData { 42 - _FakeFeedData({required this.feed, this.cursor}); 43 - 44 - final List<FeedViewPost> feed; 45 - final String? cursor; 46 - } 47 - 48 - class _FakeFeedResponse { 49 - _FakeFeedResponse(this.data); 50 - 51 - final _FakeFeedData data; 52 - } 53 - 54 - class _HandlerFeedApi { 55 - _HandlerFeedApi({required this.getTimelineHandler}); 56 - 57 - final Future<_FakeFeedResponse> Function({String? cursor, int? limit, Map<String, String>? headers}) 58 - getTimelineHandler; 59 - 60 - Future<_FakeFeedResponse> getTimeline({String? cursor, int? limit, Map<String, String>? $headers}) { 61 - return getTimelineHandler(cursor: cursor, limit: limit, headers: $headers); 62 - } 63 - } 64 - 65 - class _FakeBluesky { 66 - _FakeBluesky(this.feed); 67 - 68 - final dynamic feed; 69 - } 70 - 71 - void main() { 72 - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 73 - 74 - const homeFeedState = FeedPreferencesState.loaded( 75 - feeds: [ 76 - SavedFeed( 77 - id: 'timeline', 78 - type: SavedFeedType.knownValue(data: KnownSavedFeedType.timeline), 79 - value: 'timeline', 80 - pinned: true, 81 - ), 82 - ], 83 - ); 84 - 85 - SettingsState settingsState(FeedLayout architecture) => SettingsState( 86 - themePalette: AppThemePalette.oxocarbon, 87 - themeVariant: AppThemeVariant.dark, 88 - useSystemTheme: false, 89 - feedLayout: architecture, 90 - ); 91 - 92 - testWidgets('home feed recovers from expired token unauthorized response', (tester) async { 93 - final database = AppDatabase(executor: NativeDatabase.memory()); 94 - addTearDown(database.close); 95 - 96 - final feedPreferencesCubit = MockFeedPreferencesCubit(); 97 - final connectivityCubit = MockConnectivityCubit(); 98 - final settingsCubit = MockSettingsCubit(); 99 - final authBloc = MockAuthBloc(); 100 - final savedPostsCubit = MockSavedPostsCubit(); 101 - final postActionRepository = MockPostActionRepository(); 102 - 103 - when(() => feedPreferencesCubit.state).thenReturn(homeFeedState); 104 - whenListen(feedPreferencesCubit, const Stream<FeedPreferencesState>.empty(), initialState: homeFeedState); 105 - 106 - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); 107 - whenListen( 108 - connectivityCubit, 109 - const Stream<ConnectivityState>.empty(), 110 - initialState: const ConnectivityState.online(), 111 - ); 112 - 113 - when(() => settingsCubit.state).thenReturn(settingsState(FeedLayout.card)); 114 - whenListen(settingsCubit, const Stream<SettingsState>.empty(), initialState: settingsState(FeedLayout.card)); 115 - 116 - const authState = AuthState.authenticated( 117 - AuthTokens( 118 - accessToken: 'access-token', 119 - refreshToken: 'refresh-token', 120 - did: 'did:plc:test', 121 - handle: 'test.bsky.social', 122 - ), 123 - ); 124 - when(() => authBloc.state).thenReturn(authState); 125 - whenListen(authBloc, const Stream<AuthState>.empty(), initialState: authState); 126 - 127 - const savedPostsState = SavedPostsState(status: SavedPostsStatus.loaded); 128 - when(() => savedPostsCubit.state).thenReturn(savedPostsState); 129 - whenListen(savedPostsCubit, const Stream<SavedPostsState>.empty(), initialState: savedPostsState); 130 - 131 - var primaryTimelineCalls = 0; 132 - var fallbackTimelineCalls = 0; 133 - var authRecoveryCalls = 0; 134 - 135 - final primaryFeedApi = _HandlerFeedApi( 136 - getTimelineHandler: ({String? cursor, int? limit, Map<String, String>? headers}) async { 137 - primaryTimelineCalls += 1; 138 - throw _unauthorizedException('app.bsky.feed.getTimeline'); 139 - }, 140 - ); 141 - 142 - final fallbackFeedApi = _HandlerFeedApi( 143 - getTimelineHandler: ({String? cursor, int? limit, Map<String, String>? headers}) async { 144 - fallbackTimelineCalls += 1; 145 - return _FakeFeedResponse(_FakeFeedData(feed: [_post(1)], cursor: null)); 146 - }, 147 - ); 148 - 149 - final repository = FeedRepository( 150 - bluesky: _FakeBluesky(primaryFeedApi), 151 - database: database, 152 - accountDid: 'did:plc:test', 153 - onUnauthorized: () async { 154 - authRecoveryCalls += 1; 155 - return _freshTokens(); 156 - }, 157 - blueskyClientFactory: (_) => _FakeBluesky(fallbackFeedApi), 158 - ); 159 - 160 - await tester.pumpWidget( 161 - MaterialApp( 162 - home: MultiRepositoryProvider( 163 - providers: [ 164 - RepositoryProvider<FeedRepository>.value(value: repository), 165 - RepositoryProvider<PostActionRepository>.value(value: postActionRepository), 166 - RepositoryProvider<PostActionCache>(create: (_) => PostActionCache()), 167 - ], 168 - child: MultiBlocProvider( 169 - providers: [ 170 - BlocProvider<AuthBloc>.value(value: authBloc), 171 - BlocProvider<SettingsCubit>.value(value: settingsCubit), 172 - BlocProvider<FeedPreferencesCubit>.value(value: feedPreferencesCubit), 173 - BlocProvider<ConnectivityCubit>.value(value: connectivityCubit), 174 - BlocProvider<SavedPostsCubit>.value(value: savedPostsCubit), 175 - ], 176 - child: const HomeFeedScreen(), 177 - ), 178 - ), 179 - ), 180 - ); 181 - 182 - await tester.pump(); 183 - 184 - await _pumpUntil( 185 - tester, 186 - condition: () => find.byType(PostCardWithActions).evaluate().isNotEmpty, 187 - timeout: const Duration(seconds: 5), 188 - ); 189 - 190 - expect(primaryTimelineCalls, 1); 191 - expect(authRecoveryCalls, 1); 192 - expect(fallbackTimelineCalls, 1); 193 - expect(find.byType(PostCardWithActions), findsOneWidget); 194 - expect(find.textContaining('Failed to load feed'), findsNothing); 195 - }); 196 - } 197 - 198 - Future<void> _pumpUntil( 199 - WidgetTester tester, { 200 - required bool Function() condition, 201 - Duration timeout = const Duration(seconds: 3), 202 - }) async { 203 - final deadline = DateTime.now().add(timeout); 204 - while (!condition()) { 205 - if (DateTime.now().isAfter(deadline)) { 206 - fail('Timed out waiting for condition in integration test'); 207 - } 208 - await tester.pump(const Duration(milliseconds: 50)); 209 - } 210 - } 211 - 212 - FeedViewPost _post(int index) { 213 - final timestamp = DateTime.utc(2026, 5, 4, 12).subtract(Duration(minutes: index)); 214 - final did = 'did:plc:author$index'; 215 - return FeedViewPost( 216 - post: PostView( 217 - uri: AtUri('at://$did/app.bsky.feed.post/$index'), 218 - cid: 'cid-$index', 219 - author: ProfileViewBasic(did: did, handle: 'author$index.bsky.social'), 220 - record: { 221 - r'$type': 'app.bsky.feed.post', 222 - 'text': 'Recovered post $index', 223 - 'createdAt': timestamp.toIso8601String(), 224 - }, 225 - indexedAt: timestamp, 226 - ), 227 - ); 228 - } 229 - 230 - AuthTokens _freshTokens() { 231 - final now = DateTime.now().toUtc(); 232 - return AuthTokens( 233 - accessToken: 'fresh-access-token', 234 - refreshToken: 'fresh-refresh-token', 235 - expiresAt: now.add(const Duration(hours: 1)), 236 - did: 'did:plc:test', 237 - handle: 'test.bsky.social', 238 - service: 'bsky.social', 239 - ); 240 - } 241 - 242 - UnauthorizedException _unauthorizedException(String methodId) { 243 - return UnauthorizedException( 244 - XRPCResponse( 245 - headers: const {}, 246 - status: HttpStatus.unauthorized, 247 - request: XRPCRequest(method: HttpMethod.get, url: Uri.https('bsky.social', '/xrpc/$methodId')), 248 - rateLimit: RateLimit.unlimited(), 249 - data: const XRPCError(error: 'Unauthorized', message: 'exp claim timestamp check failed'), 250 - ), 251 - ); 252 - }
-67
integration_test/oauth_callback_https_integration_test.dart
··· 1 - import 'package:bloc_test/bloc_test.dart'; 2 - import 'package:flutter/material.dart'; 3 - import 'package:flutter_bloc/flutter_bloc.dart'; 4 - import 'package:flutter_test/flutter_test.dart'; 5 - import 'package:go_router/go_router.dart'; 6 - import 'package:integration_test/integration_test.dart'; 7 - import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 8 - import 'package:lazurite/features/auth/presentation/oauth_callback_screen.dart'; 9 - import 'package:mocktail/mocktail.dart'; 10 - 11 - class MockAuthBloc extends MockBloc<AuthEvent, AuthState> implements AuthBloc {} 12 - 13 - void main() { 14 - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 15 - 16 - setUpAll(() { 17 - registerFallbackValue(Uri.parse('https://example.com/oauth/callback')); 18 - }); 19 - 20 - testWidgets('forwards HTTPS callback URI query params and returns to login', (tester) async { 21 - final authBloc = MockAuthBloc(); 22 - when(() => authBloc.state).thenReturn(const AuthState.unauthenticated()); 23 - whenListen(authBloc, const Stream<AuthState>.empty(), initialState: const AuthState.unauthenticated()); 24 - 25 - Uri? capturedUri; 26 - when(() => authBloc.handleOAuthRedirectUri(any())).thenAnswer((invocation) async { 27 - capturedUri = invocation.positionalArguments.first as Uri; 28 - return true; 29 - }); 30 - 31 - final router = GoRouter( 32 - initialLocation: OAuthCallbackScreen.routePath, 33 - routes: [ 34 - GoRoute( 35 - path: OAuthCallbackScreen.routePath, 36 - builder: (context, state) => OAuthCallbackScreen( 37 - callbackUri: Uri.parse( 38 - 'https://lazurite.stormlightlabs.org/oauth/callback?code=abc&state=xyz&iss=https%3A%2F%2Fbsky.social', 39 - ), 40 - ), 41 - ), 42 - GoRoute( 43 - path: '/login', 44 - builder: (context, state) => const Scaffold(body: Text('login')), 45 - ), 46 - ], 47 - ); 48 - 49 - await tester.pumpWidget( 50 - BlocProvider<AuthBloc>.value( 51 - value: authBloc, 52 - child: MaterialApp.router(routerConfig: router), 53 - ), 54 - ); 55 - await tester.pumpAndSettle(); 56 - 57 - verify(() => authBloc.handleOAuthRedirectUri(any())).called(1); 58 - expect(capturedUri, isNotNull); 59 - expect(capturedUri!.scheme, equals('https')); 60 - expect(capturedUri!.host, equals('lazurite.stormlightlabs.org')); 61 - expect(capturedUri!.path, equals('/oauth/callback')); 62 - expect(capturedUri!.queryParameters['code'], equals('abc')); 63 - expect(capturedUri!.queryParameters['state'], equals('xyz')); 64 - expect(capturedUri!.queryParameters['iss'], equals('https://bsky.social')); 65 - expect(find.text('login'), findsOneWidget); 66 - }); 67 - }
-6
ios/Podfile.lock
··· 96 96 - GoogleUtilities/Privacy 97 97 - image_picker_ios (0.0.1): 98 98 - Flutter 99 - - integration_test (0.0.1): 100 - - Flutter 101 99 - nanopb (3.30910.0): 102 100 - nanopb/decode (= 3.30910.0) 103 101 - nanopb/encode (= 3.30910.0) ··· 185 183 - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) 186 184 - gal (from `.symlinks/plugins/gal/darwin`) 187 185 - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) 188 - - integration_test (from `.symlinks/plugins/integration_test/ios`) 189 186 - objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`) 190 187 - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 191 188 - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) ··· 236 233 :path: ".symlinks/plugins/gal/darwin" 237 234 image_picker_ios: 238 235 :path: ".symlinks/plugins/image_picker_ios/ios" 239 - integration_test: 240 - :path: ".symlinks/plugins/integration_test/ios" 241 236 objectbox_flutter_libs: 242 237 :path: ".symlinks/plugins/objectbox_flutter_libs/ios" 243 238 package_info_plus: ··· 281 276 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 282 277 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 283 278 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 284 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e 285 279 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 286 280 ObjectBox: eccb95ea2054c39d81dfa2d4ccc5f1e31187228a 287 281 objectbox_flutter_libs: ed1510f71602e4a0d3f2a721324e468d066fdbb9
-10
justfile
··· 29 29 just objectbox-check 30 30 flutter test {{ paths }} --fail-fast --timeout=120s 31 31 32 - # Run end-to-end style integration tests from integration_test/ 33 - e2e: 34 - just objectbox-check 35 - flutter test integration_test --reporter=failures-only --fail-fast --timeout=180s 36 - 37 - # Run one specific end-to-end test file 38 - e2e-file path: 39 - just objectbox-check 40 - flutter test {{ path }} --reporter=failures-only --fail-fast --timeout=180s 41 - 42 32 generate: 43 33 flutter pub run build_runner build --delete-conflicting-outputs 44 34
-39
pubspec.lock
··· 590 590 url: "https://pub.dev" 591 591 source: hosted 592 592 version: "3.4.1" 593 - flutter_driver: 594 - dependency: transitive 595 - description: flutter 596 - source: sdk 597 - version: "0.0.0" 598 593 flutter_lints: 599 594 dependency: "direct dev" 600 595 description: ··· 693 688 url: "https://pub.dev" 694 689 source: hosted 695 690 version: "4.0.0" 696 - fuchsia_remote_debug_protocol: 697 - dependency: transitive 698 - description: flutter 699 - source: sdk 700 - version: "0.0.0" 701 691 gal: 702 692 dependency: "direct main" 703 693 description: ··· 850 840 url: "https://pub.dev" 851 841 source: hosted 852 842 version: "0.2.2" 853 - integration_test: 854 - dependency: "direct dev" 855 - description: flutter 856 - source: sdk 857 - version: "0.0.0" 858 843 intl: 859 844 dependency: "direct main" 860 845 description: ··· 1263 1248 url: "https://pub.dev" 1264 1249 source: hosted 1265 1250 version: "1.5.2" 1266 - process: 1267 - dependency: transitive 1268 - description: 1269 - name: process 1270 - sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 1271 - url: "https://pub.dev" 1272 - source: hosted 1273 - version: "5.0.5" 1274 1251 provider: 1275 1252 dependency: "direct main" 1276 1253 description: ··· 1516 1493 url: "https://pub.dev" 1517 1494 source: hosted 1518 1495 version: "1.4.1" 1519 - sync_http: 1520 - dependency: transitive 1521 - description: 1522 - name: sync_http 1523 - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" 1524 - url: "https://pub.dev" 1525 - source: hosted 1526 - version: "0.3.1" 1527 1496 synchronized: 1528 1497 dependency: transitive 1529 1498 description: ··· 1788 1757 url: "https://pub.dev" 1789 1758 source: hosted 1790 1759 version: "3.0.3" 1791 - webdriver: 1792 - dependency: transitive 1793 - description: 1794 - name: webdriver 1795 - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" 1796 - url: "https://pub.dev" 1797 - source: hosted 1798 - version: "3.1.0" 1799 1760 webkit_inspection_protocol: 1800 1761 dependency: transitive 1801 1762 description:
-2
pubspec.yaml
··· 61 61 dev_dependencies: 62 62 flutter_test: 63 63 sdk: flutter 64 - integration_test: 65 - sdk: flutter 66 64 flutter_lints: ^6.0.0 67 65 drift_dev: ^2.24.0 68 66 build_runner: ^2.4.15