import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shuffle/core/routing/app_router.dart'; import 'package:shuffle/features/auth/domain/auth_status.dart'; import 'package:shuffle/features/auth/domain/registration_input.dart'; import 'package:shuffle/features/auth/presentation/providers/auth_providers.dart'; import 'package:shuffle/features/auth/presentation/providers/auth_redirect.dart'; import 'package:supabase_flutter/supabase_flutter.dart' hide AuthUser; import '../../../helpers/fakes.dart'; import '../../../helpers/pump_app.dart'; import '../../../helpers/router_helpers.dart'; import '../data/auth_test_support.dart'; void main() { late SupabaseMocks mocks; late StreamController events; late Session? session; late int signedOutCalls; setUp(() { mocks = SupabaseMocks(); events = StreamController.broadcast(sync: true); session = null; signedOutCalls = 0; when(() => mocks.auth.currentSession).thenAnswer((_) => session); when(() => mocks.auth.onAuthStateChange).thenAnswer((_) => events.stream); }); tearDown(() => events.close()); void emit(AuthChangeEvent event, {Session? withSession}) { session = withSession; events.add(AuthState(event, withSession)); } Future start(WidgetTester tester) async { final container = await pumpShuffleApp( tester, overrides: testOverrides( supabase: mocks.client, overrides: [ ...authOverrides(), privateRoutePathsProvider.overrideWithValue(const ['/private']), signedOutHandlersProvider.overrideWithValue([ (ref) => signedOutCalls++, ]), ], ), ); return (container: container, router: container.read(appRouterProvider)); } group('KTO-01 Kriterium 5: authStatusProvider', () { testWidgets('maps events and starts with the stored session', ( tester, ) async { session = testSession(); final (:container, router: _) = await start(tester); expect(container.read(authStatusProvider).value, isA()); emit(AuthChangeEvent.signedOut); await tester.pumpAndSettle(); expect(container.read(authStatusProvider).value, isA()); emit(AuthChangeEvent.signedIn, withSession: testSession()); await tester.pumpAndSettle(); expect(container.read(authStatusProvider).value, isA()); }); }); group('Sicherheit Kriterium 2: router redirects', () { testWidgets('private route while signed out -> /login?from=', ( tester, ) async { final (container: _, :router) = await start(tester); router.go('/private?tab=1'); await tester.pumpAndSettle(); expect(currentPath(router), '/login'); expect(currentUri(router).queryParameters['from'], '/private?tab=1'); expect(find.byKey(const Key('auth_placeholder_login')), findsOneWidget); }); testWidgets('signing in on /login returns to from', (tester) async { final (container: _, :router) = await start(tester); router.go('/private'); await tester.pumpAndSettle(); emit(AuthChangeEvent.signedIn, withSession: testSession()); await tester.pumpAndSettle(); expect(currentPath(router), '/private'); }); testWidgets('signed in: login and register lead to the start page', ( tester, ) async { session = testSession(); final (container: _, :router) = await start(tester); router.go('/login'); await tester.pumpAndSettle(); expect(currentPath(router), '/'); router.go('/register'); await tester.pumpAndSettle(); expect(currentPath(router), '/'); }); testWidgets('placeholder routes of spec 5.3 exist', (tester) async { final (container: _, :router) = await start(tester); for (final (path, key) in [ ('/login', 'login'), ('/register', 'register'), ('/register/check-email', 'check_email'), ('/forgot-password', 'forgot_password'), ('/auth/confirm?token_hash=x&type=email', 'auth_confirm'), ]) { router.go(path); await tester.pumpAndSettle(); expect( find.byKey(Key('auth_placeholder_$key')), findsOneWidget, reason: path, ); } }); }); group('KTO-01 Kriterium 8, 9: recovery guard and sign-out hook', () { testWidgets('recovery forces /reset-password until the password changed', ( tester, ) async { final (container: _, :router) = await start(tester); emit(AuthChangeEvent.passwordRecovery, withSession: testSession()); await tester.pumpAndSettle(); expect(currentPath(router), '/reset-password'); expect( find.byKey(const Key('auth_placeholder_reset_password')), findsOneWidget, ); router.go('/'); await tester.pumpAndSettle(); expect(currentPath(router), '/reset-password'); emit(AuthChangeEvent.tokenRefreshed, withSession: testSession()); await tester.pumpAndSettle(); expect(currentPath(router), '/reset-password'); emit(AuthChangeEvent.userUpdated, withSession: testSession()); await tester.pumpAndSettle(); expect(currentPath(router), '/'); }); testWidgets('aborting the recovery (signOut) leaves /reset-password', ( tester, ) async { final (container: _, :router) = await start(tester); emit(AuthChangeEvent.passwordRecovery, withSession: testSession()); await tester.pumpAndSettle(); emit(AuthChangeEvent.signedOut); await tester.pumpAndSettle(); expect(currentPath(router), '/login'); }); testWidgets('onSignedOut runs on signed-in -> signed-out only', ( tester, ) async { await start(tester); expect(signedOutCalls, 0, reason: 'initial signed-out state'); emit(AuthChangeEvent.signedIn, withSession: testSession()); await tester.pumpAndSettle(); expect(signedOutCalls, 0); emit(AuthChangeEvent.signedOut); await tester.pumpAndSettle(); expect(signedOutCalls, 1); emit(AuthChangeEvent.signedOut); await tester.pumpAndSettle(); expect(signedOutCalls, 1, reason: 'no transition'); }); testWidgets('a failed refresh on a private route ends on /login', ( tester, ) async { session = testSession(); final (container: _, :router) = await start(tester); router.go('/private'); await tester.pumpAndSettle(); expect(currentPath(router), '/private'); emit(AuthChangeEvent.signedOut); await tester.pumpAndSettle(); expect(currentPath(router), '/login'); expect(signedOutCalls, 1); }); }); group('KTO-01 Kriterium 1: registrationInputFactory', () { testWidgets('fills terms_version from AppConfig and the app language', ( tester, ) async { final (:container, router: _) = await start(tester); final input = container.read(registrationInputFactoryProvider)( email: 'a@b.de', password: 'pw', displayName: 'Anna', ageConfirmed: true, termsAccepted: true, ); expect( input, const RegistrationInput( email: 'a@b.de', password: 'pw', displayName: 'Anna', ageConfirmed: true, termsAccepted: true, termsVersion: 'test-1', locale: 'en', ), ); }); }); } typedef ProviderContainerAndRouter = ({ ProviderContainer container, GoRouter router, });