From 864527cbe87a961159560150909aeb82bed2071c Mon Sep 17 00:00:00 2001 From: karelinejones Date: Wed, 15 Apr 2026 16:55:49 -0400 Subject: [PATCH 1/2] changed email verification to using code instead of email link --- lib/core/router/app_router.dart | 19 +- .../email_verification/controller.dart | 99 +- .../email_verification.dart | 207 ++- lib/features/login/controller.dart | 24 +- lib/features/sign_up/sign_up.dart | 10 +- lib/logic/services/auth_service.dart | 11 +- .../supabase/supabase_auth_service.dart | 45 +- supabase/config.toml | 2 +- test/features/login/login_test.dart | 346 ++--- .../authentication/authenticator_test.dart | 1113 +++++++++-------- 10 files changed, 1114 insertions(+), 762 deletions(-) diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index d7d40faf..4bf10f8d 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -90,13 +90,16 @@ class RouterService { ), GoRoute( path: '/email-verification', - pageBuilder: (context, state) => CustomTransitionPage( - key: state.pageKey, - child: EmailVerificationPage(appLinks: AppLinks()), - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero, - transitionsBuilder: (_, _, _, child) => child, - ), + pageBuilder: (context, state) { + final email = state.extra is String ? state.extra as String : null; + return CustomTransitionPage( + key: state.pageKey, + child: EmailVerificationPage(email: email), + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + transitionsBuilder: (_, _, _, child) => child, + ); + }, ), GoRoute( path: '/homePage', @@ -195,7 +198,7 @@ class RouterService { transitionsBuilder: (_, _, _, child) => child, ); }, - ) + ), ], errorBuilder: (context, state) { final uri = state.uri; diff --git a/lib/features/email_verification/controller.dart b/lib/features/email_verification/controller.dart index 9a62c31c..b6719140 100644 --- a/lib/features/email_verification/controller.dart +++ b/lib/features/email_verification/controller.dart @@ -1,65 +1,86 @@ -import 'dart:async'; -import 'package:app_links/app_links.dart'; import 'package:clean_stream_laundry_app/logic/enums/authentication_response_enum.dart'; import 'package:clean_stream_laundry_app/logic/services/auth_service.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; -import 'package:go_router/go_router.dart'; + +enum EmailVerifyResult { success, invalid, error } + +enum EmailResendResult { success, failed, error } class EmailVerificationController { final AuthService _authService = GetIt.instance(); - final AppLinks appLinks; - final BuildContext context; - StreamSubscription? _authSub; - StreamSubscription? _linkSub; + final TextEditingController codeController = TextEditingController(); bool resent = false; bool isLoading = false; AuthenticationResponses? lastResponse; + String? error; - EmailVerificationController({ - required this.appLinks, - required this.context, - }); - - void init() { - _authSub = _authService.onAuthChange.listen((isLoggedIn) { - if (isLoggedIn && _authService.isEmailVerified()) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) context.go('/homePage'); - }); - } - }); - - _linkSub = appLinks.uriLinkStream.listen(_handleUri); - } + EmailVerificationController(); void dispose() { - _authSub?.cancel(); - _linkSub?.cancel(); + codeController.dispose(); } - Future _handleUri(Uri? uri) async { - if (uri != null && - uri.scheme == 'clean-stream' && - uri.host == 'email-verification') { - await _authService.getSessionFromURI(uri); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) context.go('/homePage'); - }); - } - } - - Future resendVerification() async { + Future resendVerification({String? email}) async { if (resent) return; isLoading = true; - lastResponse = await _authService.resendVerification(); + lastResponse = await _authService.resendVerification(email: email); isLoading = false; if (lastResponse == AuthenticationResponses.success) { resent = true; } } -} \ No newline at end of file + + String? get currentEmail => _authService.getCurrentUserEmail(); + + void clearError() { + error = null; + } + + Future verifyEmailCode(String email) async { + final code = codeController.text.trim(); + + if (code.length != 6) { + error = 'Please enter the 6-digit code'; + return EmailVerifyResult.invalid; + } + + isLoading = true; + error = null; + + try { + final response = await _authService.verifyEmailCode( + email: email, + code: code, + ); + + if (response == AuthenticationResponses.success) { + return EmailVerifyResult.success; + } + + error = 'Invalid or expired code'; + return EmailVerifyResult.invalid; + } catch (_) { + error = 'Something went wrong. Try again'; + return EmailVerifyResult.error; + } finally { + isLoading = false; + } + } + + Future resendVerificationEmail(String email) async { + try { + await resendVerification(email: email); + if (lastResponse == AuthenticationResponses.success) { + return EmailResendResult.success; + } + return EmailResendResult.failed; + } catch (_) { + return EmailResendResult.error; + } + } +} diff --git a/lib/features/email_verification/email_verification.dart b/lib/features/email_verification/email_verification.dart index 1acaf1d8..7b2aedaa 100644 --- a/lib/features/email_verification/email_verification.dart +++ b/lib/features/email_verification/email_verification.dart @@ -1,13 +1,13 @@ -import 'package:app_links/app_links.dart'; import 'package:clean_stream_laundry_app/features/email_verification/controller.dart'; -import 'package:clean_stream_laundry_app/features/email_verification/widgets/resend_verification.dart'; +import 'package:clean_stream_laundry_app/features/verify_code/widgets/code_field.dart'; import 'package:clean_stream_laundry_app/core/theme/theme.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; class EmailVerificationPage extends StatefulWidget { - final AppLinks appLinks; + final String? email; - const EmailVerificationPage({super.key, required this.appLinks}); + const EmailVerificationPage({super.key, this.email}); @override State createState() => _EmailVerificationPageState(); @@ -19,11 +19,7 @@ class _EmailVerificationPageState extends State { @override void initState() { super.initState(); - _controller = EmailVerificationController( - appLinks: widget.appLinks, - context: context, - ); - _controller.init(); + _controller = EmailVerificationController(); } @override @@ -32,48 +28,183 @@ class _EmailVerificationPageState extends State { super.dispose(); } - void _refresh() { + void _showMessage(String msg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); + } + + String? _resolveEmail() { + final fromRoute = widget.email?.trim(); + if (fromRoute != null && fromRoute.isNotEmpty) { + return fromRoute; + } + + final fromSession = _controller.currentEmail?.trim(); + if (fromSession != null && fromSession.isNotEmpty) { + return fromSession; + } + + return null; + } + + Future _onVerifyPressed() async { + final email = _resolveEmail(); + if (email == null) { + _showMessage('Missing account email. Please log in and try again.'); + return; + } + + setState(() {}); + final result = await _controller.verifyEmailCode(email); + if (!mounted) return; + setState(() {}); + if (result == EmailVerifyResult.success) { + _showMessage('Email verified successfully!'); + context.go('/homePage'); + } + } + + Future _onResendPressed() async { + final email = _resolveEmail(); + if (email == null) { + _showMessage('Missing account email. Please log in and try again.'); + return; + } + + setState(() {}); + final result = await _controller.resendVerificationEmail(email); + if (!mounted) return; + + setState(() {}); + switch (result) { + case EmailResendResult.success: + _showMessage('Verification code sent! Check your email.'); + break; + case EmailResendResult.failed: + _showMessage('Failed to send verification code.'); + break; + case EmailResendResult.error: + _showMessage('Error sending verification code.'); + break; + } } @override Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final email = _resolveEmail(); + return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: Center( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.email, size: 80, color: Colors.blueAccent), - const SizedBox(height: 24), - Text( - 'Please verify your email address', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.fontInverted, + backgroundColor: scheme.surface, + appBar: AppBar( + backgroundColor: scheme.surface, + foregroundColor: scheme.fontInverted, + title: const Text('Verify Email'), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => context.go('/login'), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 32), + Icon( + Icons.mark_email_read_outlined, + size: 80, + color: scheme.primary, + ), + const SizedBox(height: 32), + Text( + 'Verify your email', + style: + Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: scheme.fontInverted, + ) ?? + TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: scheme.fontInverted, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + 'Enter the 6-digit verification code we sent to your email address.', + style: + Theme.of(context).textTheme.bodyMedium?.copyWith( + color: scheme.fontSecondary, + ) ?? + TextStyle(color: scheme.fontSecondary), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + email ?? 'your email', + style: TextStyle( + fontWeight: FontWeight.w600, + color: scheme.primary, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + VerificationCodeField( + controller: _controller.codeController, + error: _controller.error, + onChanged: (_) { + _controller.clearError(); + setState(() {}); + }, + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _controller.isLoading ? null : _onVerifyPressed, + style: ElevatedButton.styleFrom( + backgroundColor: scheme.primary, + foregroundColor: scheme.onPrimary, + padding: const EdgeInsets.symmetric(vertical: 14), ), + child: _controller.isLoading + ? const SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Verify Email'), ), - const SizedBox(height: 16), + ), + const SizedBox(height: 16), + if (_controller.error != null) Text( - 'Check your inbox and click the verification link.', + _controller.error!, + style: const TextStyle(color: Colors.red), textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.fontSecondary, - ), ), - const SizedBox(height: 24), - ResendVerificationWidget( - controller: _controller, - onStateChange: _refresh, + const SizedBox(height: 8), + TextButton( + onPressed: _controller.isLoading ? null : _onResendPressed, + child: Text( + 'Resend code', + style: TextStyle(color: scheme.primary), + ), + ), + TextButton( + onPressed: _controller.isLoading + ? null + : () => context.go('/login'), + child: Text( + 'Back to Login', + style: TextStyle(color: scheme.primary), ), - ], - ), + ), + ], ), ), ); } -} \ No newline at end of file +} diff --git a/lib/features/login/controller.dart b/lib/features/login/controller.dart index 461c0b8d..b480caec 100644 --- a/lib/features/login/controller.dart +++ b/lib/features/login/controller.dart @@ -11,11 +11,9 @@ class LoginController extends ChangeNotifier { final AuthService authService; final ProfileService profileService; - LoginController({ - AuthService? authService, - ProfileService? profileService, - }) : authService = authService ?? GetIt.instance(), - profileService = profileService ?? GetIt.instance(); + LoginController({AuthService? authService, ProfileService? profileService}) + : authService = authService ?? GetIt.instance(), + profileService = profileService ?? GetIt.instance(); final TextEditingController emailController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); @@ -49,11 +47,11 @@ class LoginController extends ChangeNotifier { if (!context.mounted) return; final currentUser = authService.getCurrentUser(); if (currentUser != null) { - final name = currentUser.userMetadata?['full_name'] ?? + final name = + currentUser.userMetadata?['full_name'] ?? currentUser.userMetadata?['name'] ?? currentUser.userMetadata?['given_name']; - await profileService.createAccount( - id: currentUser.id, name: name); + await profileService.createAccount(id: currentUser.id, name: name); } if (!context.mounted) return; context.go('/homePage'); @@ -72,8 +70,10 @@ class LoginController extends ChangeNotifier { scrollController.dispose(); } - Future handleLogin(BuildContext context, - void Function(String) showMessage) async { + Future handleLogin( + BuildContext context, + void Function(String) showMessage, + ) async { final email = emailController.text.trim(); final password = passwordController.text; @@ -90,7 +90,7 @@ class LoginController extends ChangeNotifier { showMessage('Logged in as $email'); context.go('/homePage'); } else if (response == AuthenticationResponses.emailNotVerified) { - context.go('/email-Verification'); + context.go('/email-verification', extra: email); } else { setErrorColors(); } @@ -110,4 +110,4 @@ class LoginController extends ChangeNotifier { obscurePassword = !obscurePassword; notifyListeners(); } -} \ No newline at end of file +} diff --git a/lib/features/sign_up/sign_up.dart b/lib/features/sign_up/sign_up.dart index 7202ba7f..000a8d2b 100644 --- a/lib/features/sign_up/sign_up.dart +++ b/lib/features/sign_up/sign_up.dart @@ -38,8 +38,7 @@ class SignUpPageState extends State { } void _showMessage(String text) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(text))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text))); } Future _onSubmit() async { @@ -49,7 +48,10 @@ class SignUpPageState extends State { if (result.success) { _showMessage('Account created successfully.'); - context.go('/email-verification'); + context.go( + '/email-verification', + extra: _controller.emailController.text.trim(), + ); return; } @@ -146,4 +148,4 @@ class SignUpPageState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/logic/services/auth_service.dart b/lib/logic/services/auth_service.dart index 99146e30..9cdd3f4c 100644 --- a/lib/logic/services/auth_service.dart +++ b/lib/logic/services/auth_service.dart @@ -12,7 +12,7 @@ abstract class AuthService { Future isLoggedIn(); String? get getCurrentUserId; String getLastSignedUpUserId(); - Future resendVerification(); + Future resendVerification({String? email}); Stream get onAuthChange; bool isEmailVerified(); Future resetPassword(String email); @@ -28,5 +28,12 @@ abstract class AuthService { }); Future exchangeCodeForSession(String code); Future updatePassword(String newPassword); - Future verifyCode({required String email, required String code}); + Future verifyCode({ + required String email, + required String code, + }); + Future verifyEmailCode({ + required String email, + required String code, + }); } diff --git a/lib/services/supabase/supabase_auth_service.dart b/lib/services/supabase/supabase_auth_service.dart index c813fa82..57a1d5af 100644 --- a/lib/services/supabase/supabase_auth_service.dart +++ b/lib/services/supabase/supabase_auth_service.dart @@ -82,12 +82,16 @@ class SupabaseAuthService implements AuthService { final AuthResponse response = await _client.auth.signUp( email: email, password: password, - emailRedirectTo: 'clean-stream://email-verification', data: {"full_name": name}, ); if (response.user != null) { lastSignedUpUserId = response.user!.id; + try { + await _client.auth.resend(type: OtpType.signup, email: email); + } catch (e) { + print('Error sending verification OTP: $e'); + } output = AuthenticationResponses.success; } } else { @@ -157,10 +161,11 @@ class SupabaseAuthService implements AuthService { return output; } - Future resendVerification() async { + @override + Future resendVerification({String? email}) async { AuthenticationResponses output = AuthenticationResponses.success; - final userEmail = _client.auth.currentUser?.email; + final userEmail = email ?? _client.auth.currentUser?.email; try { if (userEmail != null) { @@ -266,9 +271,7 @@ class SupabaseAuthService implements AuthService { AuthenticationResponses output = AuthenticationResponses.failure; try { // Send password reset email and redirect back to the app via deep link. - await _client.auth.resetPasswordForEmail( - email - ); + await _client.auth.resetPasswordForEmail(email); output = AuthenticationResponses.success; } catch (e) { print('resetPassword error: $e'); @@ -306,7 +309,10 @@ class SupabaseAuthService implements AuthService { } @override - Future verifyCode({required String email, required String code}) async { + Future verifyCode({ + required String email, + required String code, + }) async { AuthenticationResponses output = AuthenticationResponses.success; try { @@ -319,11 +325,34 @@ class SupabaseAuthService implements AuthService { if (response.session == null) { output = AuthenticationResponses.failure; } - }catch (e){ + } catch (e) { output = AuthenticationResponses.failure; } return output; } + @override + Future verifyEmailCode({ + required String email, + required String code, + }) async { + AuthenticationResponses output = AuthenticationResponses.success; + + try { + final response = await _client.auth.verifyOTP( + email: email, + token: code, + type: OtpType.signup, + ); + + if (response.session == null) { + output = AuthenticationResponses.failure; + } + } catch (e) { + output = AuthenticationResponses.failure; + } + + return output; + } } diff --git a/supabase/config.toml b/supabase/config.toml index 06284e21..79ccbf86 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -206,7 +206,7 @@ enable_signup = true # addresses. If disabled, only the new email is required to confirm. double_confirm_changes = true # If enabled, users need to confirm their email address before signing in. -enable_confirmations = false +enable_confirmations = true # If enabled, users will need to reauthenticate or have logged in recently to change their password. secure_password_change = false # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. diff --git a/test/features/login/login_test.dart b/test/features/login/login_test.dart index e5b26ddc..f6c5855e 100644 --- a/test/features/login/login_test.dart +++ b/test/features/login/login_test.dart @@ -49,9 +49,8 @@ void main() { builder: (_, __) => const Scaffold(body: Text('Home Page')), ), GoRoute( - path: '/email-Verification', - builder: (_, __) => - const Scaffold(body: Text('Email Verification')), + path: '/email-verification', + builder: (_, __) => const Scaffold(body: Text('Verify your email')), ), GoRoute( path: '/signup', @@ -60,7 +59,7 @@ void main() { GoRoute( path: '/password-reset', builder: (_, __) => - const Scaffold(body: Text('Password Reset Page')), + const Scaffold(body: Text('Password Reset Page')), ), GoRoute( path: '/login', @@ -72,18 +71,21 @@ void main() { } void mockLoginResponse(AuthenticationResponses response) { - when(() => mockAuthService.login(any(), any())) - .thenAnswer((_) async => response); + when( + () => mockAuthService.login(any(), any()), + ).thenAnswer((_) async => response); } Future enterCredentials( - WidgetTester tester, { - String email = 'test@example.com', - String password = 'password123', - }) async { + WidgetTester tester, { + String email = 'test@example.com', + String password = 'password123', + }) async { await tester.enterText(find.widgetWithText(TextField, 'Email'), email); await tester.enterText( - find.widgetWithText(TextField, 'Password'), password); + find.widgetWithText(TextField, 'Password'), + password, + ); } group('Static UI', () { @@ -172,23 +174,26 @@ void main() { }); group('Login functionality', () { - testWidgets('shows error snackbar when both fields are empty', - (tester) async { - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); + testWidgets('shows error snackbar when both fields are empty', ( + tester, + ) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(ElevatedButton, 'Log In')); - await tester.pump(); + await tester.tap(find.widgetWithText(ElevatedButton, 'Log In')); + await tester.pump(); - expect(find.text('Please fill in both fields.'), findsOneWidget); - }); + expect(find.text('Please fill in both fields.'), findsOneWidget); + }); testWidgets('shows error snackbar when email is empty', (tester) async { await tester.pumpWidget(createWidget()); await tester.pumpAndSettle(); await tester.enterText( - find.widgetWithText(TextField, 'Password'), 'password123'); + find.widgetWithText(TextField, 'Password'), + 'password123', + ); await tester.tap(find.widgetWithText(ElevatedButton, 'Log In')); await tester.pump(); @@ -200,26 +205,29 @@ void main() { await tester.pumpAndSettle(); await tester.enterText( - find.widgetWithText(TextField, 'Email'), 'test@example.com'); + find.widgetWithText(TextField, 'Email'), + 'test@example.com', + ); await tester.tap(find.widgetWithText(ElevatedButton, 'Log In')); await tester.pump(); expect(find.text('Please fill in both fields.'), findsOneWidget); }); - testWidgets('shows logging in snackbar when credentials entered', - (tester) async { - mockLoginResponse(AuthenticationResponses.success); + testWidgets('shows logging in snackbar when credentials entered', ( + tester, + ) async { + mockLoginResponse(AuthenticationResponses.success); - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); - await enterCredentials(tester); - await tester.tap(find.widgetWithText(ElevatedButton, 'Log In')); - await tester.pump(); + await enterCredentials(tester); + await tester.tap(find.widgetWithText(ElevatedButton, 'Log In')); + await tester.pump(); - expect(find.text('Logging in as test@example.com...'), findsOneWidget); - }); + expect(find.text('Logging in as test@example.com...'), findsOneWidget); + }); testWidgets('navigates to home page on successful login', (tester) async { mockLoginResponse(AuthenticationResponses.success); @@ -232,23 +240,25 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Home Page'), findsOneWidget); - verify(() => mockAuthService.login('test@example.com', 'password123')) - .called(1); + verify( + () => mockAuthService.login('test@example.com', 'password123'), + ).called(1); }); - testWidgets('navigates to email verification on unverified email', - (tester) async { - mockLoginResponse(AuthenticationResponses.emailNotVerified); + testWidgets('navigates to email verification on unverified email', ( + tester, + ) async { + mockLoginResponse(AuthenticationResponses.emailNotVerified); - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); - await enterCredentials(tester); - await tester.tap(find.widgetWithText(ElevatedButton, 'Log In')); - await tester.pumpAndSettle(); + await enterCredentials(tester); + await tester.tap(find.widgetWithText(ElevatedButton, 'Log In')); + await tester.pumpAndSettle(); - expect(find.text('Email Verification'), findsOneWidget); - }); + expect(find.text('Verify your email'), findsOneWidget); + }); testWidgets('shows error colors on failed login', (tester) async { mockLoginResponse(AuthenticationResponses.failure); @@ -304,30 +314,33 @@ void main() { }); group('Password visibility', () { - testWidgets('toggles password visibility when suffix icon tapped', - (tester) async { - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); + testWidgets('toggles password visibility when suffix icon tapped', ( + tester, + ) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); - expect( - tester - .widget(find.widgetWithText(TextField, 'Password')) - .obscureText, - isTrue, - ); + expect( + tester + .widget(find.widgetWithText(TextField, 'Password')) + .obscureText, + isTrue, + ); - await tester.tap(find.byIcon(Icons.visibility_off)); - await tester.pump(); + await tester.tap(find.byIcon(Icons.visibility_off)); + await tester.pump(); - expect( - tester - .widget(find.widgetWithText(TextField, 'Password')) - .obscureText, - isFalse, - ); - }); + expect( + tester + .widget(find.widgetWithText(TextField, 'Password')) + .obscureText, + isFalse, + ); + }); - testWidgets('shows visibility icon when password is hidden', (tester) async { + testWidgets('shows visibility icon when password is hidden', ( + tester, + ) async { await tester.pumpWidget(createWidget()); await tester.pumpAndSettle(); @@ -335,20 +348,20 @@ void main() { expect(find.byIcon(Icons.visibility), findsNothing); }); - testWidgets('shows visibility_off icon when password is shown', - (tester) async { - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); + testWidgets('shows visibility_off icon when password is shown', ( + tester, + ) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.visibility_off)); - await tester.pump(); + await tester.tap(find.byIcon(Icons.visibility_off)); + await tester.pump(); - expect(find.byIcon(Icons.visibility), findsOneWidget); - expect(find.byIcon(Icons.visibility_off), findsNothing); - }); + expect(find.byIcon(Icons.visibility), findsOneWidget); + expect(find.byIcon(Icons.visibility_off), findsNothing); + }); }); - group('Navigation', () { testWidgets('navigates to sign_up on Create Account tap', (tester) async { await tester.pumpWidget(createWidget()); @@ -361,29 +374,30 @@ void main() { expect(find.text('Sign Up Page'), findsOneWidget); }); - testWidgets('navigates to password reset on Reset Password tap', - (tester) async { - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); + testWidgets('navigates to password reset on Reset Password tap', ( + tester, + ) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); - await tester.ensureVisible(find.text('Reset Password')); - await tester.tap(find.text('Reset Password')); - await tester.pumpAndSettle(); + await tester.ensureVisible(find.text('Reset Password')); + await tester.tap(find.text('Reset Password')); + await tester.pumpAndSettle(); - expect(find.text('Password Reset Page'), findsOneWidget); - }); + expect(find.text('Password Reset Page'), findsOneWidget); + }); }); group('Keyboard enter', () { testWidgets('pressing Enter triggers login', (tester) async { - when(() => mockAuthService.login(any(), any())) - .thenAnswer((_) async => AuthenticationResponses.success); + when( + () => mockAuthService.login(any(), any()), + ).thenAnswer((_) async => AuthenticationResponses.success); await tester.pumpWidget(createWidget()); await tester.pumpAndSettle(); - await tester.enterText( - find.byType(TextField).first, 'test@example.com'); + await tester.enterText(find.byType(TextField).first, 'test@example.com'); await tester.enterText(find.byType(TextField).last, 'password123'); await tester.pump(); @@ -398,68 +412,76 @@ void main() { }); group('Deep links', () { - testWidgets('navigates to home on email-verification deep link', - (tester) async { - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); - - fakeAppLinks.emit(Uri.parse('clean-stream://email-verification')); - await tester.pumpAndSettle(); - - expect(find.text('Home Page'), findsOneWidget); - }); - - testWidgets('handles oauth deep link with successful session', - (tester) async { - when(() => mockAuthService.getSessionFromURI(any())) - .thenAnswer((_) async {}); - when(() => mockAuthService.isLoggedIn()) - .thenAnswer((_) async => AuthenticationResponses.success); - when(() => mockAuthService.getCurrentUser()).thenReturn( - User( - id: 'testId', - appMetadata: {}, - userMetadata: {'full_name': 'Test User'}, - aud: '', - createdAt: '', - ), - ); - when(() => mockProfileService.createAccount( - id: any(named: 'id'), - name: any(named: 'name'), - )).thenAnswer((_) async {}); - - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); - - fakeAppLinks.emit(Uri.parse('clean-stream://oauth')); - await tester.pumpAndSettle(); - - expect(find.text('Home Page'), findsOneWidget); - verify(() => mockAuthService.getSessionFromURI(any())).called(1); - verify(() => mockAuthService.isLoggedIn()).called(1); - verify(() => mockAuthService.getCurrentUser()).called(1); - verify(() => mockProfileService.createAccount( - id: 'testId', - name: 'Test User', - )).called(1); - }); - - testWidgets('navigates to login on oauth deep link with failed session', - (tester) async { - when(() => mockAuthService.getSessionFromURI(any())) - .thenAnswer((_) async {}); - when(() => mockAuthService.isLoggedIn()) - .thenAnswer((_) async => AuthenticationResponses.failure); - - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); - - fakeAppLinks.emit(Uri.parse('clean-stream://oauth')); - await tester.pumpAndSettle(); - - expect(find.text('Login Page'), findsOneWidget); - }); + testWidgets('navigates to home on email-verification deep link', ( + tester, + ) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); + + fakeAppLinks.emit(Uri.parse('clean-stream://email-verification')); + await tester.pumpAndSettle(); + + expect(find.text('Home Page'), findsOneWidget); + }); + + testWidgets('handles oauth deep link with successful session', ( + tester, + ) async { + when( + () => mockAuthService.getSessionFromURI(any()), + ).thenAnswer((_) async {}); + when( + () => mockAuthService.isLoggedIn(), + ).thenAnswer((_) async => AuthenticationResponses.success); + when(() => mockAuthService.getCurrentUser()).thenReturn( + User( + id: 'testId', + appMetadata: {}, + userMetadata: {'full_name': 'Test User'}, + aud: '', + createdAt: '', + ), + ); + when( + () => mockProfileService.createAccount( + id: any(named: 'id'), + name: any(named: 'name'), + ), + ).thenAnswer((_) async {}); + + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); + + fakeAppLinks.emit(Uri.parse('clean-stream://oauth')); + await tester.pumpAndSettle(); + + expect(find.text('Home Page'), findsOneWidget); + verify(() => mockAuthService.getSessionFromURI(any())).called(1); + verify(() => mockAuthService.isLoggedIn()).called(1); + verify(() => mockAuthService.getCurrentUser()).called(1); + verify( + () => mockProfileService.createAccount(id: 'testId', name: 'Test User'), + ).called(1); + }); + + testWidgets('navigates to login on oauth deep link with failed session', ( + tester, + ) async { + when( + () => mockAuthService.getSessionFromURI(any()), + ).thenAnswer((_) async {}); + when( + () => mockAuthService.isLoggedIn(), + ).thenAnswer((_) async => AuthenticationResponses.failure); + + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); + + fakeAppLinks.emit(Uri.parse('clean-stream://oauth')); + await tester.pumpAndSettle(); + + expect(find.text('Login Page'), findsOneWidget); + }); testWidgets('ignores deep link with null uri', (tester) async { await tester.pumpWidget(createWidget()); @@ -469,19 +491,19 @@ void main() { }); }); - group('Styling', () { - testWidgets('Log In button has blue background and white text', - (tester) async { - await tester.pumpWidget(createWidget()); - await tester.pumpAndSettle(); - - final button = tester.widget( - find.widgetWithText(ElevatedButton, 'Log In'), - ); - expect(button.style?.backgroundColor?.resolve({}), Colors.blue); - expect(button.style?.foregroundColor?.resolve({}), Colors.white); - }); + testWidgets('Log In button has blue background and white text', ( + tester, + ) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); + + final button = tester.widget( + find.widgetWithText(ElevatedButton, 'Log In'), + ); + expect(button.style?.backgroundColor?.resolve({}), Colors.blue); + expect(button.style?.foregroundColor?.resolve({}), Colors.white); + }); testWidgets('Create Account text is blue and underlined', (tester) async { await tester.pumpWidget(createWidget()); @@ -500,8 +522,8 @@ void main() { find.widgetWithText(TextField, 'Email'), ); final border = - (field.decoration as InputDecoration).border as OutlineInputBorder; + (field.decoration as InputDecoration).border as OutlineInputBorder; expect(border.borderRadius, BorderRadius.circular(12)); }); }); -} \ No newline at end of file +} diff --git a/test/services/supabase/authentication/authenticator_test.dart b/test/services/supabase/authentication/authenticator_test.dart index 772642de..83125d3f 100644 --- a/test/services/supabase/authentication/authenticator_test.dart +++ b/test/services/supabase/authentication/authenticator_test.dart @@ -6,7 +6,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:mocktail/mocktail.dart'; import 'mocks.dart'; -void main(){ +void main() { late SupabaseAuthService authenticator; late SupabaseMock client; late GoTrueMock supabaseAuth; @@ -19,8 +19,7 @@ void main(){ }); group("authentication Tests", () { - - setUp((){ + setUp(() { client = SupabaseMock(); supabaseAuth = GoTrueMock(); when(() => client.auth).thenReturn(supabaseAuth); @@ -43,55 +42,26 @@ void main(){ ); when(() => supabaseAuth.currentUser).thenReturn(mockUser); - when(() => supabaseAuth.refreshSession()).thenAnswer((_) async => AuthResponse()); + when( + () => supabaseAuth.refreshSession(), + ).thenAnswer((_) async => AuthResponse()); when(() => supabaseAuth.signOut()).thenAnswer((_) async {}); - + when( + () => supabaseAuth.resend( + type: OtpType.signup, + email: any(named: 'email'), + ), + ).thenAnswer((_) async => ResendResponse()); }); - test("Tests if login is successful",()async{ - - when(() => supabaseAuth.signInWithPassword( - email: any(named: 'email'), - password: any(named: 'password'), - )).thenAnswer((_) async => AuthResponse( - user: const User( - id: '11111111-1111-1111-1111-111111111111', - aud: 'authenticated', - role: 'authenticated', - email: 'example@email.com', - emailConfirmedAt: '2024-01-01T00:00:00Z', - phone: '', - lastSignInAt: '2024-01-01T00:00:00Z', - appMetadata: { - 'provider': 'email', - 'providers': ['email'] - }, - userMetadata: {}, - identities: [ - UserIdentity( - identityId: '22222222-2222-2222-2222-222222222222', - id: '11111111-1111-1111-1111-111111111111', - userId: '11111111-1111-1111-1111-111111111111', - identityData: { - 'email': 'example@email.com', - 'email_verified': false, - 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' - }, - provider: 'email', - lastSignInAt: '2024-01-01T00:00:00Z', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ), - ], - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', + test("Tests if login is successful", () async { + when( + () => supabaseAuth.signInWithPassword( + email: any(named: 'email'), + password: any(named: 'password'), ), - session: Session( - accessToken: '', - tokenType: 'bearer', - expiresIn: 3600, - refreshToken: '', + ).thenAnswer( + (_) async => AuthResponse( user: const User( id: '11111111-1111-1111-1111-111111111111', aud: 'authenticated', @@ -102,7 +72,7 @@ void main(){ lastSignInAt: '2024-01-01T00:00:00Z', appMetadata: { 'provider': 'email', - 'providers': ['email'] + 'providers': ['email'], }, userMetadata: {}, identities: [ @@ -114,35 +84,114 @@ void main(){ 'email': 'example@email.com', 'email_verified': false, 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' + 'sub': '11111111-1111-1111-1111-111111111111', }, provider: 'email', lastSignInAt: '2024-01-01T00:00:00Z', createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', - ) + ), ], createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', ), + session: Session( + accessToken: '', + tokenType: 'bearer', + expiresIn: 3600, + refreshToken: '', + user: const User( + id: '11111111-1111-1111-1111-111111111111', + aud: 'authenticated', + role: 'authenticated', + email: 'example@email.com', + emailConfirmedAt: '2024-01-01T00:00:00Z', + phone: '', + lastSignInAt: '2024-01-01T00:00:00Z', + appMetadata: { + 'provider': 'email', + 'providers': ['email'], + }, + userMetadata: {}, + identities: [ + UserIdentity( + identityId: '22222222-2222-2222-2222-222222222222', + id: '11111111-1111-1111-1111-111111111111', + userId: '11111111-1111-1111-1111-111111111111', + identityData: { + 'email': 'example@email.com', + 'email_verified': false, + 'phone_verified': false, + 'sub': '11111111-1111-1111-1111-111111111111', + }, + provider: 'email', + lastSignInAt: '2024-01-01T00:00:00Z', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + ], + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + ), ), - ), ); - final response = await authenticator.login("testemail@test.com","testpassword"); + final response = await authenticator.login( + "testemail@test.com", + "testpassword", + ); expect(response, AuthenticationResponses.success); }); - test("Sign up test",() async{ - - when(() => supabaseAuth.signUp( + test("Sign up test", () async { + when( + () => supabaseAuth.signUp( email: any(named: 'email'), password: any(named: 'password'), data: {"full_name": "testname"}, - emailRedirectTo: 'clean-stream://email-verification' - )).thenAnswer((_) async => - AuthResponse( + ), + ).thenAnswer( + (_) async => AuthResponse( + user: const User( + id: '11111111-1111-1111-1111-111111111111', + aud: 'authenticated', + role: 'authenticated', + email: 'example@email.com', + emailConfirmedAt: '2024-01-01T00:00:00Z', + phone: '', + lastSignInAt: '2024-01-01T00:00:00Z', + appMetadata: { + 'provider': 'email', + 'providers': ['email'], + }, + userMetadata: {}, + identities: [ + UserIdentity( + identityId: '22222222-2222-2222-2222-222222222222', + id: '11111111-1111-1111-1111-111111111111', + userId: '11111111-1111-1111-1111-111111111111', + identityData: { + 'email': 'example@email.com', + 'email_verified': false, + 'phone_verified': false, + 'sub': '11111111-1111-1111-1111-111111111111', + }, + provider: 'email', + lastSignInAt: '2024-01-01T00:00:00Z', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + ], + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + session: Session( + accessToken: '', + tokenType: 'bearer', + expiresIn: 3600, + refreshToken: '', user: const User( id: '11111111-1111-1111-1111-111111111111', aud: 'authenticated', @@ -153,7 +202,7 @@ void main(){ lastSignInAt: '2024-01-01T00:00:00Z', appMetadata: { 'provider': 'email', - 'providers': ['email'] + 'providers': ['email'], }, userMetadata: {}, identities: [ @@ -165,7 +214,7 @@ void main(){ 'email': 'example@email.com', 'email_verified': false, 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' + 'sub': '11111111-1111-1111-1111-111111111111', }, provider: 'email', lastSignInAt: '2024-01-01T00:00:00Z', @@ -176,61 +225,65 @@ void main(){ createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', ), - session: Session( - accessToken: '', - tokenType: 'bearer', - expiresIn: 3600, - refreshToken: '', - user: const User( - id: '11111111-1111-1111-1111-111111111111', - aud: 'authenticated', - role: 'authenticated', - email: 'example@email.com', - emailConfirmedAt: '2024-01-01T00:00:00Z', - phone: '', - lastSignInAt: '2024-01-01T00:00:00Z', - appMetadata: { - 'provider': 'email', - 'providers': ['email'] - }, - userMetadata: {}, - identities: [ - UserIdentity( - identityId: '22222222-2222-2222-2222-222222222222', - id: '11111111-1111-1111-1111-111111111111', - userId: '11111111-1111-1111-1111-111111111111', - identityData: { - 'email': 'example@email.com', - 'email_verified': false, - 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' - }, - provider: 'email', - lastSignInAt: '2024-01-01T00:00:00Z', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ) - ], - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ), - ), ), + ), ); - final response = await authenticator.signUp("testemail", "testpassword123G@", "testname"); + final response = await authenticator.signUp( + "testemail", + "testpassword123G@", + "testname", + ); - expect(response,AuthenticationResponses.success); + expect(response, AuthenticationResponses.success); }); - test("Sign up test has no digit",() async{ - - when(() => supabaseAuth.signUp( + test("Sign up test has no digit", () async { + when( + () => supabaseAuth.signUp( email: any(named: 'email'), password: any(named: 'password'), - emailRedirectTo: 'clean-stream://email-verification' - )).thenAnswer((_) async => - AuthResponse( + ), + ).thenAnswer( + (_) async => AuthResponse( + user: const User( + id: '11111111-1111-1111-1111-111111111111', + aud: 'authenticated', + role: 'authenticated', + email: 'example@email.com', + emailConfirmedAt: '2024-01-01T00:00:00Z', + phone: '', + lastSignInAt: '2024-01-01T00:00:00Z', + appMetadata: { + 'provider': 'email', + 'providers': ['email'], + }, + userMetadata: {}, + identities: [ + UserIdentity( + identityId: '22222222-2222-2222-2222-222222222222', + id: '11111111-1111-1111-1111-111111111111', + userId: '11111111-1111-1111-1111-111111111111', + identityData: { + 'email': 'example@email.com', + 'email_verified': false, + 'phone_verified': false, + 'sub': '11111111-1111-1111-1111-111111111111', + }, + provider: 'email', + lastSignInAt: '2024-01-01T00:00:00Z', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + ], + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + session: Session( + accessToken: '', + tokenType: 'bearer', + expiresIn: 3600, + refreshToken: '', user: const User( id: '11111111-1111-1111-1111-111111111111', aud: 'authenticated', @@ -241,7 +294,7 @@ void main(){ lastSignInAt: '2024-01-01T00:00:00Z', appMetadata: { 'provider': 'email', - 'providers': ['email'] + 'providers': ['email'], }, userMetadata: {}, identities: [ @@ -253,7 +306,7 @@ void main(){ 'email': 'example@email.com', 'email_verified': false, 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' + 'sub': '11111111-1111-1111-1111-111111111111', }, provider: 'email', lastSignInAt: '2024-01-01T00:00:00Z', @@ -264,61 +317,65 @@ void main(){ createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', ), - session: Session( - accessToken: '', - tokenType: 'bearer', - expiresIn: 3600, - refreshToken: '', - user: const User( - id: '11111111-1111-1111-1111-111111111111', - aud: 'authenticated', - role: 'authenticated', - email: 'example@email.com', - emailConfirmedAt: '2024-01-01T00:00:00Z', - phone: '', - lastSignInAt: '2024-01-01T00:00:00Z', - appMetadata: { - 'provider': 'email', - 'providers': ['email'] - }, - userMetadata: {}, - identities: [ - UserIdentity( - identityId: '22222222-2222-2222-2222-222222222222', - id: '11111111-1111-1111-1111-111111111111', - userId: '11111111-1111-1111-1111-111111111111', - identityData: { - 'email': 'example@email.com', - 'email_verified': false, - 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' - }, - provider: 'email', - lastSignInAt: '2024-01-01T00:00:00Z', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ) - ], - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ), - ), ), + ), ); - final response = await authenticator.signUp("testemail", "testpasswordG", "testname"); + final response = await authenticator.signUp( + "testemail", + "testpasswordG", + "testname", + ); - expect(response,AuthenticationResponses.noDigit); + expect(response, AuthenticationResponses.noDigit); }); - test("Sign up test no special character",() async{ - - when(() => supabaseAuth.signUp( + test("Sign up test no special character", () async { + when( + () => supabaseAuth.signUp( email: any(named: 'email'), password: any(named: 'password'), - emailRedirectTo: 'clean-stream://email-verification' - )).thenAnswer((_) async => - AuthResponse( + ), + ).thenAnswer( + (_) async => AuthResponse( + user: const User( + id: '11111111-1111-1111-1111-111111111111', + aud: 'authenticated', + role: 'authenticated', + email: 'example@email.com', + emailConfirmedAt: '2024-01-01T00:00:00Z', + phone: '', + lastSignInAt: '2024-01-01T00:00:00Z', + appMetadata: { + 'provider': 'email', + 'providers': ['email'], + }, + userMetadata: {}, + identities: [ + UserIdentity( + identityId: '22222222-2222-2222-2222-222222222222', + id: '11111111-1111-1111-1111-111111111111', + userId: '11111111-1111-1111-1111-111111111111', + identityData: { + 'email': 'example@email.com', + 'email_verified': false, + 'phone_verified': false, + 'sub': '11111111-1111-1111-1111-111111111111', + }, + provider: 'email', + lastSignInAt: '2024-01-01T00:00:00Z', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + ], + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + session: Session( + accessToken: '', + tokenType: 'bearer', + expiresIn: 3600, + refreshToken: '', user: const User( id: '11111111-1111-1111-1111-111111111111', aud: 'authenticated', @@ -329,7 +386,7 @@ void main(){ lastSignInAt: '2024-01-01T00:00:00Z', appMetadata: { 'provider': 'email', - 'providers': ['email'] + 'providers': ['email'], }, userMetadata: {}, identities: [ @@ -341,7 +398,7 @@ void main(){ 'email': 'example@email.com', 'email_verified': false, 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' + 'sub': '11111111-1111-1111-1111-111111111111', }, provider: 'email', lastSignInAt: '2024-01-01T00:00:00Z', @@ -352,61 +409,65 @@ void main(){ createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', ), - session: Session( - accessToken: '', - tokenType: 'bearer', - expiresIn: 3600, - refreshToken: '', - user: const User( - id: '11111111-1111-1111-1111-111111111111', - aud: 'authenticated', - role: 'authenticated', - email: 'example@email.com', - emailConfirmedAt: '2024-01-01T00:00:00Z', - phone: '', - lastSignInAt: '2024-01-01T00:00:00Z', - appMetadata: { - 'provider': 'email', - 'providers': ['email'] - }, - userMetadata: {}, - identities: [ - UserIdentity( - identityId: '22222222-2222-2222-2222-222222222222', - id: '11111111-1111-1111-1111-111111111111', - userId: '11111111-1111-1111-1111-111111111111', - identityData: { - 'email': 'example@email.com', - 'email_verified': false, - 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' - }, - provider: 'email', - lastSignInAt: '2024-01-01T00:00:00Z', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ) - ], - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ), - ), ), + ), ); - final response = await authenticator.signUp("testemail", "testpassword123G", "testname"); + final response = await authenticator.signUp( + "testemail", + "testpassword123G", + "testname", + ); - expect(response,AuthenticationResponses.noSpecialCharacter); + expect(response, AuthenticationResponses.noSpecialCharacter); }); - test("Sign up test no upper case",() async{ - - when(() => supabaseAuth.signUp( + test("Sign up test no upper case", () async { + when( + () => supabaseAuth.signUp( email: any(named: 'email'), password: any(named: 'password'), - emailRedirectTo: 'clean-stream://email-verification' - )).thenAnswer((_) async => - AuthResponse( + ), + ).thenAnswer( + (_) async => AuthResponse( + user: const User( + id: '11111111-1111-1111-1111-111111111111', + aud: 'authenticated', + role: 'authenticated', + email: 'example@email.com', + emailConfirmedAt: '2024-01-01T00:00:00Z', + phone: '', + lastSignInAt: '2024-01-01T00:00:00Z', + appMetadata: { + 'provider': 'email', + 'providers': ['email'], + }, + userMetadata: {}, + identities: [ + UserIdentity( + identityId: '22222222-2222-2222-2222-222222222222', + id: '11111111-1111-1111-1111-111111111111', + userId: '11111111-1111-1111-1111-111111111111', + identityData: { + 'email': 'example@email.com', + 'email_verified': false, + 'phone_verified': false, + 'sub': '11111111-1111-1111-1111-111111111111', + }, + provider: 'email', + lastSignInAt: '2024-01-01T00:00:00Z', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + ], + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + session: Session( + accessToken: '', + tokenType: 'bearer', + expiresIn: 3600, + refreshToken: '', user: const User( id: '11111111-1111-1111-1111-111111111111', aud: 'authenticated', @@ -417,7 +478,7 @@ void main(){ lastSignInAt: '2024-01-01T00:00:00Z', appMetadata: { 'provider': 'email', - 'providers': ['email'] + 'providers': ['email'], }, userMetadata: {}, identities: [ @@ -429,7 +490,7 @@ void main(){ 'email': 'example@email.com', 'email_verified': false, 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' + 'sub': '11111111-1111-1111-1111-111111111111', }, provider: 'email', lastSignInAt: '2024-01-01T00:00:00Z', @@ -440,61 +501,65 @@ void main(){ createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', ), - session: Session( - accessToken: '', - tokenType: 'bearer', - expiresIn: 3600, - refreshToken: '', - user: const User( - id: '11111111-1111-1111-1111-111111111111', - aud: 'authenticated', - role: 'authenticated', - email: 'example@email.com', - emailConfirmedAt: '2024-01-01T00:00:00Z', - phone: '', - lastSignInAt: '2024-01-01T00:00:00Z', - appMetadata: { - 'provider': 'email', - 'providers': ['email'] - }, - userMetadata: {}, - identities: [ - UserIdentity( - identityId: '22222222-2222-2222-2222-222222222222', - id: '11111111-1111-1111-1111-111111111111', - userId: '11111111-1111-1111-1111-111111111111', - identityData: { - 'email': 'example@email.com', - 'email_verified': false, - 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' - }, - provider: 'email', - lastSignInAt: '2024-01-01T00:00:00Z', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ) - ], - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ), - ), ), + ), ); - final response = await authenticator.signUp("testemail", "testpassword123@", "testname"); + final response = await authenticator.signUp( + "testemail", + "testpassword123@", + "testname", + ); - expect(response,AuthenticationResponses.noUppercase); + expect(response, AuthenticationResponses.noUppercase); }); - test("Sign up test no digit",() async{ - - when(() => supabaseAuth.signUp( + test("Sign up test no digit", () async { + when( + () => supabaseAuth.signUp( email: any(named: 'email'), password: any(named: 'password'), - emailRedirectTo: 'clean-stream://email-verification' - )).thenAnswer((_) async => - AuthResponse( + ), + ).thenAnswer( + (_) async => AuthResponse( + user: const User( + id: '11111111-1111-1111-1111-111111111111', + aud: 'authenticated', + role: 'authenticated', + email: 'example@email.com', + emailConfirmedAt: '2024-01-01T00:00:00Z', + phone: '', + lastSignInAt: '2024-01-01T00:00:00Z', + appMetadata: { + 'provider': 'email', + 'providers': ['email'], + }, + userMetadata: {}, + identities: [ + UserIdentity( + identityId: '22222222-2222-2222-2222-222222222222', + id: '11111111-1111-1111-1111-111111111111', + userId: '11111111-1111-1111-1111-111111111111', + identityData: { + 'email': 'example@email.com', + 'email_verified': false, + 'phone_verified': false, + 'sub': '11111111-1111-1111-1111-111111111111', + }, + provider: 'email', + lastSignInAt: '2024-01-01T00:00:00Z', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + ], + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + ), + session: Session( + accessToken: '', + tokenType: 'bearer', + expiresIn: 3600, + refreshToken: '', user: const User( id: '11111111-1111-1111-1111-111111111111', aud: 'authenticated', @@ -505,7 +570,7 @@ void main(){ lastSignInAt: '2024-01-01T00:00:00Z', appMetadata: { 'provider': 'email', - 'providers': ['email'] + 'providers': ['email'], }, userMetadata: {}, identities: [ @@ -517,7 +582,7 @@ void main(){ 'email': 'example@email.com', 'email_verified': false, 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' + 'sub': '11111111-1111-1111-1111-111111111111', }, provider: 'email', lastSignInAt: '2024-01-01T00:00:00Z', @@ -528,172 +593,164 @@ void main(){ createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', ), - session: Session( - accessToken: '', - tokenType: 'bearer', - expiresIn: 3600, - refreshToken: '', - user: const User( - id: '11111111-1111-1111-1111-111111111111', - aud: 'authenticated', - role: 'authenticated', - email: 'example@email.com', - emailConfirmedAt: '2024-01-01T00:00:00Z', - phone: '', - lastSignInAt: '2024-01-01T00:00:00Z', - appMetadata: { - 'provider': 'email', - 'providers': ['email'] - }, - userMetadata: {}, - identities: [ - UserIdentity( - identityId: '22222222-2222-2222-2222-222222222222', - id: '11111111-1111-1111-1111-111111111111', - userId: '11111111-1111-1111-1111-111111111111', - identityData: { - 'email': 'example@email.com', - 'email_verified': false, - 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' - }, - provider: 'email', - lastSignInAt: '2024-01-01T00:00:00Z', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ) - ], - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - ), - ), ), + ), ); - final response = await authenticator.signUp("testemail", "test", "testname"); + final response = await authenticator.signUp( + "testemail", + "test", + "testname", + ); - expect(response,AuthenticationResponses.lessThanMinLength); + expect(response, AuthenticationResponses.lessThanMinLength); }); - test("Tests if login is unsuccessful because of invalid credentials", () async { - when(() => supabaseAuth.signInWithPassword( - email: any(named: 'email'), - password: any(named: 'password'), - )).thenAnswer((_) async => AuthResponse( - user: null, - session: null, - )); - - final response = await authenticator.login("testemail", "testpassword"); - - expect(response, AuthenticationResponses.failure); - }); + test( + "Tests if login is unsuccessful because of invalid credentials", + () async { + when( + () => supabaseAuth.signInWithPassword( + email: any(named: 'email'), + password: any(named: 'password'), + ), + ).thenAnswer((_) async => AuthResponse(user: null, session: null)); + final response = await authenticator.login("testemail", "testpassword"); - test("Tests if login is unsuccessful because email is not confirmed",()async{ + expect(response, AuthenticationResponses.failure); + }, + ); - when(() => - supabaseAuth.signInWithPassword( + test( + "Tests if login is unsuccessful because email is not confirmed", + () async { + when( + () => supabaseAuth.signInWithPassword( email: any(named: 'email'), password: any(named: 'password'), - )).thenThrow(AuthApiException("Email Not Confirmed",code:'email_not_confirmed',statusCode:"400")); - - final response = await authenticator.login("testemail", "testpassword"); + ), + ).thenThrow( + AuthApiException( + "Email Not Confirmed", + code: 'email_not_confirmed', + statusCode: "400", + ), + ); - expect(response,AuthenticationResponses.emailNotVerified); - }); + final response = await authenticator.login("testemail", "testpassword"); - test("Resend verification email unsuccessfully",() async{ + expect(response, AuthenticationResponses.emailNotVerified); + }, + ); - when(() => supabaseAuth.resend( + test("Resend verification email unsuccessfully", () async { + when( + () => supabaseAuth.resend( type: OtpType.signup, - email: any(named:"email") - )).thenThrow(AuthException("Invalid email")); + email: any(named: "email"), + ), + ).thenThrow(AuthException("Invalid email")); final response = await authenticator.resendVerification(); - expect(response,AuthenticationResponses.failure); + expect(response, AuthenticationResponses.failure); }); - test("Resend verification email succesfully",() async{ - - when(() => supabaseAuth.resend( + test("Resend verification email succesfully", () async { + when( + () => supabaseAuth.resend( type: OtpType.signup, - email: any(named:"email") - )).thenAnswer((_) async => ResendResponse()); + email: any(named: "email"), + ), + ).thenAnswer((_) async => ResendResponse()); final response = await authenticator.resendVerification(); - expect(response,AuthenticationResponses.success); + expect(response, AuthenticationResponses.success); }); - test("User is logged in",() async{ - - when(() => supabaseAuth.currentSession).thenReturn(Session(accessToken: 'test', tokenType: 'test', user: User(id: '', appMetadata: {}, userMetadata: {}, aud: '', createdAt: ''))); + test("User is logged in", () async { + when(() => supabaseAuth.currentSession).thenReturn( + Session( + accessToken: 'test', + tokenType: 'test', + user: User( + id: '', + appMetadata: {}, + userMetadata: {}, + aud: '', + createdAt: '', + ), + ), + ); final response = await authenticator.isLoggedIn(); - expect(response,AuthenticationResponses.success); + expect(response, AuthenticationResponses.success); }); - test("User is not logged in",() async{ + test("User is not logged in", () async { when(() => supabaseAuth.currentUser).thenReturn(null); final response = await authenticator.isLoggedIn(); - expect(response,AuthenticationResponses.failure); + expect(response, AuthenticationResponses.failure); }); - test("Verifying that the logged out logic was called",() async { + test("Verifying that the logged out logic was called", () async { await authenticator.logout(); verify(() => client.auth.signOut()); }); - test("Should return that an email is not verified",() async{ - when(() => supabaseAuth.signInWithPassword( - email: any(named: 'email'), - password: any(named: 'password'), - )).thenThrow(AuthApiException( - 'Email not verified', - code: 'email_not_confirmed', - )); + test("Should return that an email is not verified", () async { + when( + () => supabaseAuth.signInWithPassword( + email: any(named: 'email'), + password: any(named: 'password'), + ), + ).thenThrow( + AuthApiException('Email not verified', code: 'email_not_confirmed'), + ); - final result = await authenticator.login("testEmail","testPassword"); + final result = await authenticator.login("testEmail", "testPassword"); expect(result, AuthenticationResponses.emailNotVerified); }); - test("Test if an exception is thrown with a different code",() async{ - when(() => supabaseAuth.signInWithPassword( - email: any(named: 'email'), - password: any(named: 'password'), - )).thenThrow(AuthApiException( - 'Email not verified', - code: 'random-test-code', - )); + test("Test if an exception is thrown with a different code", () async { + when( + () => supabaseAuth.signInWithPassword( + email: any(named: 'email'), + password: any(named: 'password'), + ), + ).thenThrow( + AuthApiException('Email not verified', code: 'random-test-code'), + ); - final result = await authenticator.login("testEmail","testPassword"); + final result = await authenticator.login("testEmail", "testPassword"); expect(result, AuthenticationResponses.failure); }); - test("Test if an exception is thrown with an unkown exception",() async{ - when(() => supabaseAuth.signInWithPassword( - email: any(named: 'email'), - password: any(named: 'password'), - )).thenThrow(Exception("Unknown exception")); + test("Test if an exception is thrown with an unkown exception", () async { + when( + () => supabaseAuth.signInWithPassword( + email: any(named: 'email'), + password: any(named: 'password'), + ), + ).thenThrow(Exception("Unknown exception")); - final result = await authenticator.login("testEmail","testPassword"); + final result = await authenticator.login("testEmail", "testPassword"); expect(result, AuthenticationResponses.failure); }); - test("Test that correctID is returned",(){ + test("Test that correctID is returned", () { final result = authenticator.getCurrentUserId; expect(result, "11111111-1111-1111-1111-111111111111"); }); - test("Test that null is returned for no user being able to be found",(){ + test("Test that null is returned for no user being able to be found", () { when(() => supabaseAuth.currentUser).thenReturn(null); final result = authenticator.getCurrentUserId; expect(result, null); }); - test("Tests if the email is verified",(){ - + test("Tests if the email is verified", () { final testUser = User( id: '11111111-1111-1111-1111-111111111111', aud: 'authenticated', @@ -704,7 +761,7 @@ void main(){ lastSignInAt: '2024-01-01T00:00:00Z', appMetadata: { 'provider': 'email', - 'providers': ['email'] + 'providers': ['email'], }, userMetadata: {}, identities: [ @@ -716,7 +773,7 @@ void main(){ 'email': 'example@email.com', 'email_verified': false, 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' + 'sub': '11111111-1111-1111-1111-111111111111', }, provider: 'email', lastSignInAt: '2024-01-01T00:00:00Z', @@ -728,14 +785,12 @@ void main(){ updatedAt: '2024-01-01T00:00:00Z', ); - when(() => supabaseAuth.currentUser).thenReturn(testUser); var result = authenticator.isEmailVerified(); expect(result, true); }); - test("Tests if the email is not verified",(){ - + test("Tests if the email is not verified", () { final testUser = User( id: '11111111-1111-1111-1111-111111111111', aud: 'authenticated', @@ -746,7 +801,7 @@ void main(){ lastSignInAt: '2024-01-01T00:00:00Z', appMetadata: { 'provider': 'email', - 'providers': ['email'] + 'providers': ['email'], }, userMetadata: {}, identities: [ @@ -758,7 +813,7 @@ void main(){ 'email': 'example@email.com', 'email_verified': false, 'phone_verified': false, - 'sub': '11111111-1111-1111-1111-111111111111' + 'sub': '11111111-1111-1111-1111-111111111111', }, provider: 'email', lastSignInAt: '2024-01-01T00:00:00Z', @@ -770,7 +825,6 @@ void main(){ updatedAt: '2024-01-01T00:00:00Z', ); - when(() => supabaseAuth.currentUser).thenReturn(testUser); var result = authenticator.isEmailVerified(); expect(result, false); @@ -781,17 +835,20 @@ void main(){ final controller = StreamController(); - when(() => supabaseAuth.onAuthStateChange) - .thenAnswer((_) => Stream.value(AuthState( - AuthChangeEvent.signedIn, - Session( - accessToken: '', - tokenType: 'bearer', - expiresIn: 3600, - refreshToken: '', - user: supabaseAuth.currentUser!, + when(() => supabaseAuth.onAuthStateChange).thenAnswer( + (_) => Stream.value( + AuthState( + AuthChangeEvent.signedIn, + Session( + accessToken: '', + tokenType: 'bearer', + expiresIn: 3600, + refreshToken: '', + user: supabaseAuth.currentUser!, + ), + ), ), - ))); + ); when(() => auth.onAuthStateChange).thenAnswer((_) => controller.stream); @@ -815,74 +872,91 @@ void main(){ expiresIn: 3600, ); - expectLater(authenticator.onAuthChange, emits(true)); controller.add(AuthState(AuthChangeEvent.signedIn, fakeSession)); }); test("onAuthChange emits false when a user doesn't exist", () async { - final controller = StreamController(); - when(() => supabaseAuth.onAuthStateChange).thenAnswer((_) => controller.stream); + when( + () => supabaseAuth.onAuthStateChange, + ).thenAnswer((_) => controller.stream); expectLater(authenticator.onAuthChange, emits(false)); controller.add(AuthState(AuthChangeEvent.signedOut, null)); - }); test("googleSignIn calls signInWithOAuth", () async { - when(() => client.auth.signInWithOAuth( - any(), - redirectTo: any(named: 'redirectTo'), - )).thenAnswer((_) async => true); + when( + () => client.auth.signInWithOAuth( + any(), + redirectTo: any(named: 'redirectTo'), + ), + ).thenAnswer((_) async => true); await authenticator.googleSignIn(); - verify(() => client.auth.signInWithOAuth( - any(), - redirectTo: any(named: 'redirectTo'), - )).called(1); + verify( + () => client.auth.signInWithOAuth( + any(), + redirectTo: any(named: 'redirectTo'), + ), + ).called(1); }); - test("Tests that all errors are properly handled",() async{ - when(() => client.auth.signInWithOAuth(any())).thenThrow(Exception("Test Error")); + test("Tests that all errors are properly handled", () async { + when( + () => client.auth.signInWithOAuth(any()), + ).thenThrow(Exception("Test Error")); await authenticator.googleSignIn(); //If test reaches here it passed because nothing failed }); test("appleSignIn calls signInWithOAuth", () async { - when(() => client.auth.signInWithOAuth( - any(), - redirectTo: any(named: 'redirectTo'), - )).thenAnswer((_) async => true); - - await authenticator.appleSignIn(); - - verify(() => client.auth.signInWithOAuth( - any(), - redirectTo: any(named: 'redirectTo'), - )).called(1); - }); + when( + () => client.auth.signInWithOAuth( + any(), + redirectTo: any(named: 'redirectTo'), + ), + ).thenAnswer((_) async => true); - test("Tests that all errors are properly handled with apple sign in",() async{ - when(() => client.auth.signInWithOAuth(any())).thenThrow(Exception("Test Error")); await authenticator.appleSignIn(); - //If test reaches here it passed because nothing failed - }); - test("Tests that all errors are properly handled with google sign in",() async{ - when(() => client.auth.signInWithOAuth(any())).thenThrow(Exception("Test Error")); - await authenticator.googleSignIn(); - //If test reaches here it passed because nothing failed + verify( + () => client.auth.signInWithOAuth( + any(), + redirectTo: any(named: 'redirectTo'), + ), + ).called(1); }); + test( + "Tests that all errors are properly handled with apple sign in", + () async { + when( + () => client.auth.signInWithOAuth(any()), + ).thenThrow(Exception("Test Error")); + await authenticator.appleSignIn(); + //If test reaches here it passed because nothing failed + }, + ); + + test( + "Tests that all errors are properly handled with google sign in", + () async { + when( + () => client.auth.signInWithOAuth(any()), + ).thenThrow(Exception("Test Error")); + await authenticator.googleSignIn(); + //If test reaches here it passed because nothing failed + }, + ); test("Tests to see that the redirect was called", () async { - when(() => supabaseAuth.getSessionFromUrl(any())).thenAnswer( - (_) async => AuthSessionUrlResponse( + (_) async => AuthSessionUrlResponse( session: Session( accessToken: "test_token", tokenType: "bearer", @@ -904,137 +978,200 @@ void main(){ verify(() => supabaseAuth.getSessionFromUrl(testUri)).called(1); }); - test("Tests that the user was grabbed correctly",() async{ + test("Tests that the user was grabbed correctly", () async { await authenticator.getCurrentUser(); verify(() => client.auth.currentUser); }); - test("Tests that the correct userID is gotten",() async{ + test("Tests that the correct userID is gotten", () async { String? userID = await authenticator.getCurrentUserId; expect(userID, '11111111-1111-1111-1111-111111111111'); }); - test("Tests that as session is returned",() async{ - - when(() => supabaseAuth.getSessionFromUrl(any())).thenAnswer( (_) async => AuthSessionUrlResponse(session: Session(accessToken: "test", tokenType: "test", user: User(id: "1234", appMetadata: {}, userMetadata: {}, aud: "test", createdAt: "test")), redirectType: "test")); - + test("Tests that as session is returned", () async { + when(() => supabaseAuth.getSessionFromUrl(any())).thenAnswer( + (_) async => AuthSessionUrlResponse( + session: Session( + accessToken: "test", + tokenType: "test", + user: User( + id: "1234", + appMetadata: {}, + userMetadata: {}, + aud: "test", + createdAt: "test", + ), + ), + redirectType: "test", + ), + ); + await authenticator.getSessionFromURI(Uri()); verify(() => client.auth.getSessionFromUrl(any())); - }); - test("Verifies that a session was refreshed if it's not null",() async { - - when(() => supabaseAuth.currentSession).thenReturn(Session(accessToken: "test", tokenType: "test", user: User(id: '', appMetadata: {}, userMetadata: {}, aud: '', createdAt: ''))); + test("Verifies that a session was refreshed if it's not null", () async { + when(() => supabaseAuth.currentSession).thenReturn( + Session( + accessToken: "test", + tokenType: "test", + user: User( + id: '', + appMetadata: {}, + userMetadata: {}, + aud: '', + createdAt: '', + ), + ), + ); await authenticator.refreshSession(); verify(() => supabaseAuth.refreshSession()); }); - test("Verifies that a session was not refreshed if the session is null",() async { - when(() => supabaseAuth.currentSession).thenReturn(null); + test( + "Verifies that a session was not refreshed if the session is null", + () async { + when(() => supabaseAuth.currentSession).thenReturn(null); - await authenticator.refreshSession(); + await authenticator.refreshSession(); - verifyNever(() => supabaseAuth.refreshSession()); - }); + verifyNever(() => supabaseAuth.refreshSession()); + }, + ); - test("getCurrentUserEmail returns correct email",() async { + test("getCurrentUserEmail returns correct email", () async { when(() => supabaseAuth.currentSession).thenReturn(null); String? result = await authenticator.getCurrentUserEmail(); - expect(result,'testemail@test.com'); + expect(result, 'testemail@test.com'); }); - test("Tests that code is verified correctly",() async { - when(() => supabaseAuth.verifyOTP( - email: any(named: 'email'), - token: any(named: 'token'), - type: any(named: 'type'), - )).thenAnswer((_) async => AuthResponse( - session: Session( - accessToken: "test", - tokenType: "test", - user: User( - id: '', - appMetadata: {}, - userMetadata: {}, - aud: '', - createdAt: '', + test("Tests that code is verified correctly", () async { + when( + () => supabaseAuth.verifyOTP( + email: any(named: 'email'), + token: any(named: 'token'), + type: any(named: 'type'), + ), + ).thenAnswer( + (_) async => AuthResponse( + session: Session( + accessToken: "test", + tokenType: "test", + user: User( + id: '', + appMetadata: {}, + userMetadata: {}, + aud: '', + createdAt: '', + ), ), ), - )); + ); - AuthenticationResponses testResponse = await authenticator.verifyCode(email: "test", code: "testCode"); + AuthenticationResponses testResponse = await authenticator.verifyCode( + email: "test", + code: "testCode", + ); expect(testResponse, AuthenticationResponses.success); }); - test("Tests that failure is returned when exception is thrown",() async { - when(() => supabaseAuth.verifyOTP( - email: any(named: 'email'), - token: any(named: 'token'), - type: any(named: 'type'), - )).thenThrow(Exception()); + test("Tests that failure is returned when exception is thrown", () async { + when( + () => supabaseAuth.verifyOTP( + email: any(named: 'email'), + token: any(named: 'token'), + type: any(named: 'type'), + ), + ).thenThrow(Exception()); - AuthenticationResponses testResponse = await authenticator.verifyCode(email: "test", code: "testCode"); + AuthenticationResponses testResponse = await authenticator.verifyCode( + email: "test", + code: "testCode", + ); expect(testResponse, AuthenticationResponses.failure); }); - - test("Tests that exchange code for session runs correctly",() async { - when(() => supabaseAuth.exchangeCodeForSession(any())).thenAnswer((_) async => AuthSessionUrlResponse(session: Session(accessToken: "", tokenType: "", user: User(id: "", appMetadata: {}, userMetadata: {}, aud: "", createdAt: "")), redirectType:"")); - AuthenticationResponses response = await authenticator.exchangeCodeForSession("testCode"); + + test("Tests that exchange code for session runs correctly", () async { + when(() => supabaseAuth.exchangeCodeForSession(any())).thenAnswer( + (_) async => AuthSessionUrlResponse( + session: Session( + accessToken: "", + tokenType: "", + user: User( + id: "", + appMetadata: {}, + userMetadata: {}, + aud: "", + createdAt: "", + ), + ), + redirectType: "", + ), + ); + AuthenticationResponses response = await authenticator + .exchangeCodeForSession("testCode"); expect(response, AuthenticationResponses.success); }); - test("Tests that failure is sent with an exception",() async { - when(() => supabaseAuth.exchangeCodeForSession(any())).thenThrow(Exception()); - AuthenticationResponses response = await authenticator.exchangeCodeForSession("testCode"); + test("Tests that failure is sent with an exception", () async { + when( + () => supabaseAuth.exchangeCodeForSession(any()), + ).thenThrow(Exception()); + AuthenticationResponses response = await authenticator + .exchangeCodeForSession("testCode"); expect(response, AuthenticationResponses.failure); }); - test("Tests that update password runs correctly",() async { - - when(() => supabaseAuth.updateUser(any())).thenAnswer((_) async => UserResponse.fromJson({})); + test("Tests that update password runs correctly", () async { + when( + () => supabaseAuth.updateUser(any()), + ).thenAnswer((_) async => UserResponse.fromJson({})); - AuthenticationResponses response = await authenticator.updatePassword("password"); + AuthenticationResponses response = await authenticator.updatePassword( + "password", + ); expect(response, AuthenticationResponses.success); }); - test("Tests that update password handles errors",() async { - + test("Tests that update password handles errors", () async { when(() => supabaseAuth.updateUser(any())).thenThrow(Exception()); - AuthenticationResponses response = await authenticator.updatePassword("password"); + AuthenticationResponses response = await authenticator.updatePassword( + "password", + ); expect(response, AuthenticationResponses.failure); }); - test("Reset password runs correctly",() async { - - when(() => supabaseAuth.resetPasswordForEmail(any())) - .thenAnswer((_) async {}); + test("Reset password runs correctly", () async { + when( + () => supabaseAuth.resetPasswordForEmail(any()), + ).thenAnswer((_) async {}); - - AuthenticationResponses response = await authenticator.resetPassword("testEmail"); + AuthenticationResponses response = await authenticator.resetPassword( + "testEmail", + ); expect(response, AuthenticationResponses.success); }); - test("Reset password handles errors",() async { - - when(() => supabaseAuth.resetPasswordForEmail(any())) - .thenThrow(Exception()); + test("Reset password handles errors", () async { + when( + () => supabaseAuth.resetPasswordForEmail(any()), + ).thenThrow(Exception()); - - AuthenticationResponses response = await authenticator.resetPassword("testEmail"); + AuthenticationResponses response = await authenticator.resetPassword( + "testEmail", + ); expect(response, AuthenticationResponses.failure); }); - }); -} \ No newline at end of file +} From 48f77649a82bdd1ebfcd656011b615a68b5bfb07 Mon Sep 17 00:00:00 2001 From: karelinejones Date: Mon, 20 Apr 2026 16:54:33 -0400 Subject: [PATCH 2/2] fixed failed tests --- .../email_verification/controller_test.dart | 306 ++++++++++-------- .../email_verification_test.dart | 241 ++++++++------ .../widgets/resend_verification_test.dart | 118 +++---- 3 files changed, 377 insertions(+), 288 deletions(-) diff --git a/test/features/email_verification/controller_test.dart b/test/features/email_verification/controller_test.dart index 55502528..757d05fd 100644 --- a/test/features/email_verification/controller_test.dart +++ b/test/features/email_verification/controller_test.dart @@ -1,94 +1,66 @@ -import 'dart:async'; import 'package:clean_stream_laundry_app/features/email_verification/controller.dart'; import 'package:clean_stream_laundry_app/logic/enums/authentication_response_enum.dart'; import 'package:clean_stream_laundry_app/logic/services/auth_service.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; -import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'mocks.dart'; void main() { late MockAuthService mockAuthService; - late StreamController authChangeController; - late FakeAppLinks fakeAppLinks; setUpAll(() { - registerFallbackValue(FakeAuthService()); - registerFallbackValue(FakeUri()); + registerFallbackValue(''); }); setUp(() { mockAuthService = MockAuthService(); - authChangeController = StreamController.broadcast(); - fakeAppLinks = FakeAppLinks(); - GetIt.instance.registerSingleton(mockAuthService); - - when(() => mockAuthService.onAuthChange) - .thenAnswer((_) => authChangeController.stream); - when(() => mockAuthService.isEmailVerified()).thenReturn(false); }); tearDown(() { - authChangeController.close(); - fakeAppLinks.dispose(); GetIt.instance.reset(); }); - /// minimal GoRouter - controller needs a context - Widget buildWithRouter({ - required void Function(EmailVerificationController) onControllerReady, - }) { - return MaterialApp.router( - routerConfig: GoRouter( - initialLocation: '/email-verification', - routes: [ - GoRoute( - path: '/email-verification', - builder: (context, state) { - final controller = EmailVerificationController( - appLinks: fakeAppLinks, - context: context, - ); - onControllerReady(controller); - controller.init(); - return const SizedBox(); - }, - ), - GoRoute( - path: '/homePage', - builder: (_, __) => const Scaffold(body: Text('Home')), - ), - ], - ), - ); - } + EmailVerificationController buildController() => + EmailVerificationController(); + + // --------------------------------------------------------------------------- + // resendVerification + // --------------------------------------------------------------------------- group('resendVerification', () { - testWidgets('calls auth service resendVerification', (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.success); + test('calls auth service resendVerification', () async { + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.success); - late EmailVerificationController controller; - await tester.pumpWidget( - buildWithRouter(onControllerReady: (c) => controller = c), - ); + final controller = buildController(); await controller.resendVerification(); verify(() => mockAuthService.resendVerification()).called(1); }); - testWidgets('sets resent to true on success', (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.success); + test('passes email when provided', () async { + const email = 'verify@example.com'; + when( + () => mockAuthService.resendVerification(email: email), + ).thenAnswer((_) async => AuthenticationResponses.success); - late EmailVerificationController controller; - await tester.pumpWidget( - buildWithRouter(onControllerReady: (c) => controller = c), - ); + final controller = buildController(); + + await controller.resendVerification(email: email); + + verify(() => mockAuthService.resendVerification(email: email)).called(1); + }); + + test('sets resent to true on success', () async { + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.success); + + final controller = buildController(); await controller.resendVerification(); @@ -96,117 +68,185 @@ void main() { expect(controller.lastResponse, AuthenticationResponses.success); }); - testWidgets('sets lastResponse on failure, resent stays false', - (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.failure); + test('sets lastResponse on failure and keeps resent false', () async { + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.failure); - late EmailVerificationController controller; - await tester.pumpWidget( - buildWithRouter(onControllerReady: (c) => controller = c), - ); + final controller = buildController(); - await controller.resendVerification(); + await controller.resendVerification(); - expect(controller.resent, isFalse); - expect(controller.lastResponse, AuthenticationResponses.failure); - }); + expect(controller.resent, isFalse); + expect(controller.lastResponse, AuthenticationResponses.failure); + }); - testWidgets('does not call service again when already resent', - (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.success); + test('does not call service again when already resent', () async { + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.success); - late EmailVerificationController controller; - await tester.pumpWidget( - buildWithRouter(onControllerReady: (c) => controller = c), - ); + final controller = buildController(); - await controller.resendVerification(); - await controller.resendVerification(); + await controller.resendVerification(); + await controller.resendVerification(); - verify(() => mockAuthService.resendVerification()).called(1); - }); + verify(() => mockAuthService.resendVerification()).called(1); + }); }); - group('Auth change listener', () { - testWidgets('calls isEmailVerified when authState changes', - (tester) async { - when(() => mockAuthService.isEmailVerified()).thenReturn(true); + // --------------------------------------------------------------------------- + // verifyEmailCode + // --------------------------------------------------------------------------- + + group('verifyEmailCode', () { + test('returns invalid when code is not 6 digits', () async { + final controller = buildController(); + controller.codeController.text = '123'; + + final result = await controller.verifyEmailCode('user@example.com'); + + expect(result, EmailVerifyResult.invalid); + expect(controller.error, 'Please enter the 6-digit code'); + verifyNever( + () => mockAuthService.verifyEmailCode( + email: any(named: 'email'), + code: any(named: 'code'), + ), + ); + }); - await tester.pumpWidget(buildWithRouter(onControllerReady: (_) {})); - await tester.pumpAndSettle(); + test('returns success when auth service verifies code', () async { + when( + () => mockAuthService.verifyEmailCode( + email: 'user@example.com', + code: '123456', + ), + ).thenAnswer((_) async => AuthenticationResponses.success); - authChangeController.add(true); - await tester.pumpAndSettle(); + final controller = buildController(); + controller.codeController.text = '123456'; + final result = await controller.verifyEmailCode('user@example.com'); - verify(() => mockAuthService.isEmailVerified()).called(1); - }); + expect(result, EmailVerifyResult.success); + expect(controller.error, isNull); + expect(controller.isLoading, isFalse); + }); - testWidgets('does not navigate when logged in but email not verified', - (tester) async { - when(() => mockAuthService.isEmailVerified()).thenReturn(false); + test('returns invalid when auth service rejects code', () async { + when( + () => mockAuthService.verifyEmailCode( + email: 'user@example.com', + code: '123456', + ), + ).thenAnswer((_) async => AuthenticationResponses.failure); - await tester.pumpWidget(buildWithRouter(onControllerReady: (_) {})); - await tester.pumpAndSettle(); + final controller = buildController(); + controller.codeController.text = '123456'; - authChangeController.add(true); - await tester.pumpAndSettle(); + final result = await controller.verifyEmailCode('user@example.com'); - expect(find.text('Home'), findsNothing); - }); + expect(result, EmailVerifyResult.invalid); + expect(controller.error, 'Invalid or expired code'); + expect(controller.isLoading, isFalse); + }); - testWidgets('does not navigate when auth emits false', (tester) async { - when(() => mockAuthService.isEmailVerified()).thenReturn(true); + test('returns error when auth service throws', () async { + when( + () => mockAuthService.verifyEmailCode( + email: 'user@example.com', + code: '123456', + ), + ).thenThrow(Exception('network')); - await tester.pumpWidget(buildWithRouter(onControllerReady: (_) {})); - await tester.pumpAndSettle(); + final controller = buildController(); + controller.codeController.text = '123456'; - authChangeController.add(false); - await tester.pumpAndSettle(); + final result = await controller.verifyEmailCode('user@example.com'); - expect(find.text('Home'), findsNothing); + expect(result, EmailVerifyResult.error); + expect(controller.error, 'Something went wrong. Try again'); + expect(controller.isLoading, isFalse); }); }); - group('Deep link handling', () { - testWidgets('calls getSessionFromURI on valid deep link', (tester) async { - when(() => mockAuthService.getSessionFromURI(any())) - .thenAnswer((_) async {}); + // --------------------------------------------------------------------------- + // resendVerificationEmail + // --------------------------------------------------------------------------- + + group('resendVerificationEmail', () { + test('returns success when resend succeeds', () async { + when( + () => mockAuthService.resendVerification(email: 'user@example.com'), + ).thenAnswer((_) async => AuthenticationResponses.success); - await tester.pumpWidget(buildWithRouter(onControllerReady: (_) {})); - await tester.pumpAndSettle(); + final controller = buildController(); - fakeAppLinks.emit(Uri.parse('clean-stream://email-verification')); - await tester.pumpAndSettle(); - await tester.pump(); - await tester.pumpAndSettle(); + final result = await controller.resendVerificationEmail( + 'user@example.com', + ); - verify(() => mockAuthService.getSessionFromURI(any())).called(1); + expect(result, EmailResendResult.success); }); - testWidgets('ignores URIs with wrong host', (tester) async { - await tester.pumpWidget(buildWithRouter(onControllerReady: (_) {})); - await tester.pumpAndSettle(); + test('returns failed when resend returns non-success', () async { + when( + () => mockAuthService.resendVerification(email: 'user@example.com'), + ).thenAnswer((_) async => AuthenticationResponses.failure); + + final controller = buildController(); - fakeAppLinks.emit(Uri.parse('clean-stream://wrong-host')); - await tester.pumpAndSettle(); + final result = await controller.resendVerificationEmail( + 'user@example.com', + ); - verifyNever(() => mockAuthService.getSessionFromURI(any())); - expect(find.text('Home'), findsNothing); + expect(result, EmailResendResult.failed); }); - }); + test('returns error when resend throws', () async { + when( + () => mockAuthService.resendVerification(email: 'user@example.com'), + ).thenThrow(Exception('network')); + + final controller = buildController(); - group('Lifecycle', () { - testWidgets('dispose cancels subscriptions without error', (tester) async { - late EmailVerificationController controller; - await tester.pumpWidget( - buildWithRouter(onControllerReady: (c) => controller = c), + final result = await controller.resendVerificationEmail( + 'user@example.com', ); + expect(result, EmailResendResult.error); + }); + }); + + // --------------------------------------------------------------------------- + // helpers + // --------------------------------------------------------------------------- + + group('helpers', () { + test('currentEmail reads from auth service', () { + when( + () => mockAuthService.getCurrentUserEmail(), + ).thenReturn('user@example.com'); + + final controller = buildController(); + + expect(controller.currentEmail, 'user@example.com'); + }); + + test('clearError clears existing error', () { + final controller = buildController(); + controller.error = 'test error'; + + controller.clearError(); + + expect(controller.error, isNull); + }); + + test('dispose does not throw', () { + final controller = buildController(); + expect(() => controller.dispose(), returnsNormally); }); }); -} \ No newline at end of file +} diff --git a/test/features/email_verification/email_verification_test.dart b/test/features/email_verification/email_verification_test.dart index d8eb6e82..cd178a60 100644 --- a/test/features/email_verification/email_verification_test.dart +++ b/test/features/email_verification/email_verification_test.dart @@ -1,10 +1,6 @@ -import 'dart:async'; import 'package:clean_stream_laundry_app/features/email_verification/email_verification.dart'; +import 'package:clean_stream_laundry_app/logic/enums/authentication_response_enum.dart'; import 'package:clean_stream_laundry_app/logic/services/auth_service.dart'; -import 'package:clean_stream_laundry_app/logic/services/location_service.dart'; -import 'package:clean_stream_laundry_app/logic/services/machine_service.dart'; -import 'package:clean_stream_laundry_app/logic/services/profile_service.dart'; -import 'package:clean_stream_laundry_app/features/home/home.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; @@ -14,61 +10,41 @@ import 'mocks.dart'; void main() { late MockAuthService mockAuthService; - late StreamController authChangeController; - late MockMachineService mockMachineService; - late MockLocationService mockLocationService; - late MockProfileService mockProfileService; - late FakeAppLinks fakeAppLinks; setUpAll(() { registerFallbackValue(FakeAuthService()); - registerFallbackValue(FakeUri()); + registerFallbackValue(''); }); setUp(() { mockAuthService = MockAuthService(); - authChangeController = StreamController.broadcast(); - mockMachineService = MockMachineService(); - mockLocationService = MockLocationService(); - mockProfileService = MockProfileService(); - fakeAppLinks = FakeAppLinks(); GetIt.instance.registerSingleton(mockAuthService); - GetIt.instance.registerSingleton(mockMachineService); - GetIt.instance.registerSingleton(mockLocationService); - GetIt.instance.registerSingleton(mockProfileService); - - when(() => mockAuthService.onAuthChange) - .thenAnswer((_) => authChangeController.stream); - when(() => mockAuthService.isEmailVerified()).thenReturn(false); - when(() => mockLocationService.getLocations()) - .thenAnswer((_) async => >[]); + when(() => mockAuthService.getCurrentUserEmail()).thenReturn(null); }); tearDown(() { - authChangeController.close(); - fakeAppLinks.dispose(); GetIt.instance.reset(); }); - Widget createTestWidget() { + Widget createTestWidget({String? email}) { return MaterialApp.router( routerConfig: GoRouter( initialLocation: '/email-verification', routes: [ GoRoute( path: '/email-verification', - builder: (context, state) => - EmailVerificationPage(appLinks: fakeAppLinks), + builder: (context, state) => EmailVerificationPage(email: email), ), GoRoute( path: '/homePage', - builder: (context, state) => HomePage(), + builder: (context, state) => + const Scaffold(body: Text('Home Page')), ), GoRoute( - path: '/scanner', + path: '/login', builder: (context, state) => - const Scaffold(body: Text('Scanner Page')), + const Scaffold(body: Text('Login Page')), ), ], ), @@ -77,125 +53,194 @@ void main() { group('Static UI', () { testWidgets('displays all required UI elements', (tester) async { - await tester.pumpWidget(createTestWidget()); + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); await tester.pumpAndSettle(); - expect(find.byIcon(Icons.email), findsOneWidget); - expect(find.text('Please verify your email address'), findsOneWidget); + expect(find.byIcon(Icons.mark_email_read_outlined), findsOneWidget); + expect(find.text('Verify your email'), findsOneWidget); expect( - find.text('Check your inbox and click the verification link.'), + find.text( + 'Enter the 6-digit verification code we sent to your email address.', + ), findsOneWidget, ); - expect(find.text('Resend Verification'), findsOneWidget); + expect(find.text('Verify Email'), findsNWidgets(2)); + expect(find.text('Resend code'), findsOneWidget); + expect(find.text('Back to Login'), findsOneWidget); }); - testWidgets('email icon has correct styling', (tester) async { - await tester.pumpWidget(createTestWidget()); + testWidgets('email icon has correct sizing', (tester) async { + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); await tester.pumpAndSettle(); - final icon = tester.widget(find.byIcon(Icons.email)); + final icon = tester.widget( + find.byIcon(Icons.mark_email_read_outlined), + ); expect(icon.size, equals(80)); - expect(icon.color, equals(Colors.blueAccent)); }); - testWidgets('text uses center alignment', (tester) async { - await tester.pumpWidget(createTestWidget()); + testWidgets('shows route email when provided', (tester) async { + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); await tester.pumpAndSettle(); - final titleText = tester.widget( - find.text('Please verify your email address'), - ); - final descText = tester.widget( - find.text('Check your inbox and click the verification link.'), - ); + expect(find.text('route@example.com'), findsOneWidget); + }); - expect(titleText.textAlign, equals(TextAlign.center)); - expect(descText.textAlign, equals(TextAlign.center)); + testWidgets('shows current user email when route email is missing', ( + tester, + ) async { + when( + () => mockAuthService.getCurrentUserEmail(), + ).thenReturn('session@example.com'); + + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + expect(find.text('session@example.com'), findsOneWidget); }); - testWidgets('uses theme surface color as scaffold background', - (tester) async { - await tester.pumpWidget(createTestWidget()); - await tester.pumpAndSettle(); + testWidgets('shows fallback email placeholder when none available', ( + tester, + ) async { + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); - final scaffold = tester.widget(find.byType(Scaffold)); - expect(scaffold.backgroundColor, isNotNull); - }); + expect(find.text('your email'), findsOneWidget); + }); }); - group('Navigation', () { - testWidgets('verifies email verification check called', (tester) async { - when(() => mockAuthService.isEmailVerified()).thenReturn(true); + group('Actions', () { + testWidgets( + 'shows missing email message when verify is pressed without email', + (tester) async { + await tester.pumpWidget(createTestWidget()); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(ElevatedButton, 'Verify Email')); + await tester.pumpAndSettle(); + + expect( + find.text('Missing account email. Please log in and try again.'), + findsOneWidget, + ); + verifyNever( + () => mockAuthService.verifyEmailCode( + email: any(named: 'email'), + code: any(named: 'code'), + ), + ); + }, + ); - await tester.pumpWidget(createTestWidget()); + testWidgets('navigates to home after successful code verification', ( + tester, + ) async { + when( + () => mockAuthService.verifyEmailCode( + email: 'route@example.com', + code: '123456', + ), + ).thenAnswer((_) async => AuthenticationResponses.success); + + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); await tester.pumpAndSettle(); - authChangeController.add(true); + await tester.enterText(find.byType(TextField), '123456'); + await tester.tap(find.widgetWithText(ElevatedButton, 'Verify Email')); await tester.pumpAndSettle(); - verify(() => mockAuthService.isEmailVerified()).called(1); + expect(find.text('Home Page'), findsOneWidget); }); - testWidgets('stays on page when email not verified', (tester) async { - when(() => mockAuthService.isEmailVerified()).thenReturn(false); - - await tester.pumpWidget(createTestWidget()); + testWidgets('shows invalid code error when verification fails', ( + tester, + ) async { + when( + () => mockAuthService.verifyEmailCode( + email: 'route@example.com', + code: '123456', + ), + ).thenAnswer((_) async => AuthenticationResponses.failure); + + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); await tester.pumpAndSettle(); - authChangeController.add(true); + await tester.enterText(find.byType(TextField), '123456'); + await tester.tap(find.widgetWithText(ElevatedButton, 'Verify Email')); await tester.pumpAndSettle(); - expect(find.text('Please verify your email address'), findsOneWidget); + expect(find.text('Invalid or expired code'), findsWidgets); }); - testWidgets('stays on page when auth emits false (logout)', (tester) async { - when(() => mockAuthService.isEmailVerified()).thenReturn(true); + testWidgets('resends code and shows success snackbar', (tester) async { + when( + () => mockAuthService.resendVerification(email: 'route@example.com'), + ).thenAnswer((_) async => AuthenticationResponses.success); - await tester.pumpWidget(createTestWidget()); + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); await tester.pumpAndSettle(); - authChangeController.add(false); + await tester.tap(find.text('Resend code')); await tester.pumpAndSettle(); - expect(find.text('Please verify your email address'), findsOneWidget); + verify( + () => mockAuthService.resendVerification(email: 'route@example.com'), + ).called(1); + expect( + find.text('Verification code sent! Check your email.'), + findsOneWidget, + ); }); - testWidgets('verifies deeplink gets session', (tester) async { - when(() => mockAuthService.getSessionFromURI(any())) - .thenAnswer((_) async {}); + testWidgets('shows resend failure message', (tester) async { + when( + () => mockAuthService.resendVerification(email: 'route@example.com'), + ).thenAnswer((_) async => AuthenticationResponses.failure); - await tester.pumpWidget(createTestWidget()); + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); await tester.pumpAndSettle(); - fakeAppLinks.emit(Uri.parse('clean-stream://email-verification')); + await tester.tap(find.text('Resend code')); await tester.pumpAndSettle(); - verify(() => mockAuthService.getSessionFromURI(any())).called(1); + expect(find.text('Failed to send verification code.'), findsOneWidget); }); - testWidgets('ignores deep link with wrong host', (tester) async { - await tester.pumpWidget(createTestWidget()); + testWidgets('navigates to login when back to login is tapped', ( + tester, + ) async { + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); await tester.pumpAndSettle(); - fakeAppLinks.emit(Uri.parse('clean-stream://other-host')); + await tester.dragUntilVisible( + find.text('Back to Login'), + find.byType(SingleChildScrollView), + const Offset(0, -200), + ); + await tester.tap(find.text('Back to Login')); await tester.pumpAndSettle(); - expect(find.text('Please verify your email address'), findsOneWidget); + expect(find.text('Login Page'), findsOneWidget); }); }); - group('Lifecycle', () { - testWidgets('properly disposes controller on navigation away', - (tester) async { - await tester.pumpWidget(createTestWidget()); - await tester.pumpAndSettle(); + group('Layout', () { + testWidgets('uses theme surface color as scaffold background', ( + tester, + ) async { + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); + await tester.pumpAndSettle(); + + final scaffold = tester.widget(find.byType(Scaffold)); + expect(scaffold.backgroundColor, isNotNull); + }); - final context = - tester.element(find.byType(EmailVerificationPage)); - GoRouter.of(context).go('/scanner'); - await tester.pumpAndSettle(); + testWidgets('title text is centered', (tester) async { + await tester.pumpWidget(createTestWidget(email: 'route@example.com')); + await tester.pumpAndSettle(); - expect(find.byType(EmailVerificationPage), findsNothing); - }); + final titleText = tester.widget(find.text('Verify your email')); + expect(titleText.textAlign, TextAlign.center); + }); }); -} \ No newline at end of file +} diff --git a/test/features/email_verification/widgets/resend_verification_test.dart b/test/features/email_verification/widgets/resend_verification_test.dart index dccff93c..0254f9d7 100644 --- a/test/features/email_verification/widgets/resend_verification_test.dart +++ b/test/features/email_verification/widgets/resend_verification_test.dart @@ -10,40 +10,33 @@ import '../mocks.dart'; void main() { late MockAuthService mockAuthService; - late FakeAppLinks fakeAppLinks; setUpAll(() { - registerFallbackValue(FakeAuthService()); - registerFallbackValue(FakeUri()); + registerFallbackValue(''); }); setUp(() { mockAuthService = MockAuthService(); - fakeAppLinks = FakeAppLinks(); GetIt.instance.registerSingleton(mockAuthService); - - when(() => mockAuthService.onAuthChange) - .thenAnswer((_) => const Stream.empty()); }); tearDown(() { - fakeAppLinks.dispose(); GetIt.instance.reset(); }); Future buildController( - WidgetTester tester) async { + WidgetTester tester, + ) async { late EmailVerificationController controller; await tester.pumpWidget( MaterialApp( - home: Builder(builder: (context) { - controller = EmailVerificationController( - appLinks: fakeAppLinks, - context: context, - ); - return const SizedBox(); - }), + home: Builder( + builder: (context) { + controller = EmailVerificationController(); + return const SizedBox(); + }, + ), ), ); @@ -98,25 +91,28 @@ void main() { }); group('Success state', () { - testWidgets('shows check_circle icon after successful resend', - (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.success); + testWidgets('shows check_circle icon after successful resend', ( + tester, + ) async { + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.success); - final controller = await buildController(tester); - await tester.pumpWidget(buildWidget(controller: controller)); - await tester.pumpAndSettle(); + final controller = await buildController(tester); + await tester.pumpWidget(buildWidget(controller: controller)); + await tester.pumpAndSettle(); - await tester.tap(find.text('Resend Verification')); - await tester.pumpAndSettle(); + await tester.tap(find.text('Resend Verification')); + await tester.pumpAndSettle(); - expect(find.byIcon(Icons.check_circle), findsOneWidget); - expect(find.text('Resend Verification'), findsNothing); - }); + expect(find.byIcon(Icons.check_circle), findsOneWidget); + expect(find.text('Resend Verification'), findsNothing); + }); testWidgets('check_circle icon has correct styling', (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.success); + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.success); final controller = await buildController(tester); await tester.pumpWidget(buildWidget(controller: controller)); @@ -130,31 +126,34 @@ void main() { expect(icon.color, equals(Colors.green)); }); - testWidgets('tapping check_circle does not trigger another resend', - (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.success); + testWidgets('tapping check_circle does not trigger another resend', ( + tester, + ) async { + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.success); - final controller = await buildController(tester); - await tester.pumpWidget(buildWidget(controller: controller)); - await tester.pumpAndSettle(); + final controller = await buildController(tester); + await tester.pumpWidget(buildWidget(controller: controller)); + await tester.pumpAndSettle(); - await tester.tap(find.text('Resend Verification')); - await tester.pumpAndSettle(); + await tester.tap(find.text('Resend Verification')); + await tester.pumpAndSettle(); - verify(() => mockAuthService.resendVerification()).called(1); + verify(() => mockAuthService.resendVerification()).called(1); - await tester.tap(find.byIcon(Icons.check_circle)); - await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.check_circle)); + await tester.pumpAndSettle(); - verifyNever(() => mockAuthService.resendVerification()); - }); + verifyNever(() => mockAuthService.resendVerification()); + }); }); group('Failure state', () { testWidgets('shows close icon and error text on failure', (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.failure); + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.failure); final controller = await buildController(tester); await tester.pumpWidget(buildWidget(controller: controller)); @@ -171,8 +170,9 @@ void main() { }); testWidgets('error container has red circular decoration', (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.failure); + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.failure); final controller = await buildController(tester); await tester.pumpWidget(buildWidget(controller: controller)); @@ -184,9 +184,9 @@ void main() { final container = tester.widget( find .ancestor( - of: find.byIcon(Icons.close), - matching: find.byType(Container), - ) + of: find.byIcon(Icons.close), + matching: find.byType(Container), + ) .first, ); @@ -195,9 +195,12 @@ void main() { expect(decoration.shape, equals(BoxShape.circle)); }); - testWidgets('does not trigger another resend after failure', (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.failure); + testWidgets('does not trigger another resend after failure', ( + tester, + ) async { + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.failure); final controller = await buildController(tester); await tester.pumpWidget(buildWidget(controller: controller)); @@ -217,8 +220,9 @@ void main() { group('InkWell interaction', () { testWidgets('tapping InkWell calls resendVerification', (tester) async { - when(() => mockAuthService.resendVerification()) - .thenAnswer((_) async => AuthenticationResponses.success); + when( + () => mockAuthService.resendVerification(), + ).thenAnswer((_) async => AuthenticationResponses.success); final controller = await buildController(tester); await tester.pumpWidget(buildWidget(controller: controller)); @@ -235,4 +239,4 @@ void main() { verify(() => mockAuthService.resendVerification()).called(1); }); }); -} \ No newline at end of file +}