From dc9aedfb79a336a1e81ad3a94e67b5a9c5680f26 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sun, 28 Jun 2026 15:02:03 +0900 Subject: [PATCH] refactor: decouple user repository google auth contract --- .../google_authentication_service.dart | 101 ++++++++++++++++++ .../repositories/user_repository_impl.dart | 59 ++-------- .../entities/google_auth_credential.dart | 6 ++ lib/domain/repositories/user_repository.dart | 12 +-- .../google_sign_in_button_web.dart | 26 ++--- .../login/screens/sign_in_main_screen.dart | 11 +- .../interceptors/token_interceptor_test.dart | 18 +--- .../user_repository_impl_test.dart | 63 +++++++---- .../user_repository_boundary_test.dart | 17 +++ .../use-cases/delete_user_use_case_test.dart | 18 +--- .../reconcile_alarms_use_case_test.dart | 18 +--- .../schedule_mutation_use_cases_test.dart | 18 +--- .../domain/use-cases/user_use_cases_test.dart | 17 +-- .../screens/sign_in_main_screen_test.dart | 84 ++++++++++++++- .../my_page/delete_user_modal_test.dart | 18 +--- 15 files changed, 290 insertions(+), 196 deletions(-) create mode 100644 lib/core/services/google_authentication_service.dart create mode 100644 lib/domain/entities/google_auth_credential.dart create mode 100644 test/domain/repositories/user_repository_boundary_test.dart diff --git a/lib/core/services/google_authentication_service.dart b/lib/core/services/google_authentication_service.dart new file mode 100644 index 00000000..5eabaa18 --- /dev/null +++ b/lib/core/services/google_authentication_service.dart @@ -0,0 +1,101 @@ +import 'package:flutter/foundation.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:injectable/injectable.dart'; +import 'package:on_time_front/core/logging/app_logger.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; + +abstract interface class GoogleAuthenticationService { + Stream get authenticationCredentials; + + bool get supportsAuthenticate; + + Future initialize(); + + Future authenticate(); + + Future disconnect(); +} + +class GoogleAuthenticationCanceledException implements Exception { + const GoogleAuthenticationCanceledException(); +} + +@Singleton(as: GoogleAuthenticationService) +class GoogleSignInAuthenticationService implements GoogleAuthenticationService { + GoogleSignInAuthenticationService({@ignoreParam GoogleSignIn? googleSignIn}) + : _googleSignIn = googleSignIn ?? GoogleSignIn.instance; + + static const _googleIosClientId = + '456571312261-r35ah9qi0qaq7al007e2db0e0jmjcmb4.apps.googleusercontent.com'; + static const _googleServerClientId = + '456571312261-5kuf2r6i5i7lqjr7qealv06sdgkn3hcp.apps.googleusercontent.com'; + static const _googleScopes = ['email', 'profile']; + + final GoogleSignIn _googleSignIn; + Future? _initialization; + + @override + Stream get authenticationCredentials => _googleSignIn + .authenticationEvents + .where((event) => event is GoogleSignInAuthenticationEventSignIn) + .cast() + .map((event) => _credentialFromAccount(event.user)); + + @override + bool get supportsAuthenticate => _googleSignIn.supportsAuthenticate(); + + @override + Future initialize() { + return _initialization ??= _initialize(); + } + + Future _initialize() async { + await _googleSignIn.initialize( + clientId: _googleClientId, + serverClientId: _googleServerClientId, + ); + } + + @override + Future authenticate() async { + try { + await initialize(); + final account = await _googleSignIn.authenticate( + scopeHint: _googleScopes, + ); + return _credentialFromAccount(account); + } on GoogleSignInException catch (error) { + if (error.code == GoogleSignInExceptionCode.canceled) { + throw const GoogleAuthenticationCanceledException(); + } + rethrow; + } + } + + @override + Future disconnect() async { + try { + await _googleSignIn.disconnect(); + AppLogger.debug('Google Sign-In disconnected'); + } catch (error) { + AppLogger.debug( + 'Google Sign-In disconnect failed errorType=${error.runtimeType}', + ); + } + } + + GoogleAuthCredential _credentialFromAccount(GoogleSignInAccount account) { + final idToken = account.authentication.idToken; + if (idToken == null) { + throw Exception('Google ID Token is null'); + } + return GoogleAuthCredential(idToken: idToken); + } + + String? get _googleClientId { + if (kIsWeb) return null; + return defaultTargetPlatform == TargetPlatform.iOS + ? _googleIosClientId + : null; + } +} diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index bd42f3cc..3c26f224 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -1,65 +1,34 @@ import 'dart:async'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:injectable/injectable.dart'; import 'package:on_time_front/core/logging/app_logger.dart'; +import 'package:on_time_front/core/services/google_authentication_service.dart'; import 'package:on_time_front/core/validation/backend_constraints.dart'; import 'package:on_time_front/data/data_sources/authentication_remote_data_source.dart'; import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; import 'package:on_time_front/data/models/sign_in_with_google_request_model.dart'; import 'package:on_time_front/data/models/sign_in_with_apple_request_model.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; import 'package:on_time_front/domain/repositories/user_repository.dart'; import 'package:rxdart/subjects.dart'; @Singleton(as: UserRepository) class UserRepositoryImpl implements UserRepository { - static const _googleIosClientId = - '456571312261-r35ah9qi0qaq7al007e2db0e0jmjcmb4.apps.googleusercontent.com'; - static const _googleServerClientId = - '456571312261-5kuf2r6i5i7lqjr7qealv06sdgkn3hcp.apps.googleusercontent.com'; - static const _googleScopes = ['email', 'profile']; - final AuthenticationRemoteDataSource _authenticationRemoteDataSource; final TokenLocalDataSource _tokenLocalDataSource; - final GoogleSignIn _googleSignIn = GoogleSignIn.instance; - Future? _googleSignInInitialization; + final GoogleAuthenticationService _googleAuthenticationService; late final _userStreamController = BehaviorSubject.seeded( const UserEntity.empty(), ); - @override - Stream get googleAuthenticationEvents => - _googleSignIn.authenticationEvents; - - @override - bool get supportsGoogleAuthenticate => _googleSignIn.supportsAuthenticate(); - UserRepositoryImpl( this._authenticationRemoteDataSource, this._tokenLocalDataSource, + this._googleAuthenticationService, ); - @override - Future initializeGoogleSignIn() { - return _googleSignInInitialization ??= _initializeGoogleSignIn(); - } - - Future _initializeGoogleSignIn() async { - await _googleSignIn.initialize( - clientId: _googleClientId, - serverClientId: _googleServerClientId, - ); - } - - @override - Future authenticateWithGoogle() async { - await initializeGoogleSignIn(); - return _googleSignIn.authenticate(scopeHint: _googleScopes); - } - @override Future getUser() async { try { @@ -122,16 +91,14 @@ class UserRepositoryImpl implements UserRepository { } @override - Future signInWithGoogle(GoogleSignInAccount googleUser) async { + Future signInWithGoogle(GoogleAuthCredential credential) async { try { - final GoogleSignInAuthentication googleAuth = googleUser.authentication; - final String? idToken = googleAuth.idToken; - if (idToken == null) { + if (credential.idToken.isEmpty) { throw Exception('Google ID Token is null'); } final signInWithGoogleRequestModel = SignInWithGoogleRequestModel( - idToken: idToken, - refreshToken: '', + idToken: credential.idToken, + refreshToken: credential.refreshToken, ); await _tokenLocalDataSource.deleteToken(); final result = await _authenticationRemoteDataSource.signInWithGoogle( @@ -225,8 +192,7 @@ class UserRepositoryImpl implements UserRepository { @override Future disconnectGoogleSignIn() async { try { - await _googleSignIn.disconnect(); - AppLogger.debug('Google Sign-In disconnected'); + await _googleAuthenticationService.disconnect(); } catch (error) { AppLogger.debug( 'Google Sign-In disconnect failed errorType=${error.runtimeType}', @@ -237,11 +203,4 @@ class UserRepositoryImpl implements UserRepository { @override Stream get userStream => _userStreamController.asBroadcastStream(); - - String? get _googleClientId { - if (kIsWeb) return null; - return defaultTargetPlatform == TargetPlatform.iOS - ? _googleIosClientId - : null; - } } diff --git a/lib/domain/entities/google_auth_credential.dart b/lib/domain/entities/google_auth_credential.dart new file mode 100644 index 00000000..e444b22a --- /dev/null +++ b/lib/domain/entities/google_auth_credential.dart @@ -0,0 +1,6 @@ +class GoogleAuthCredential { + const GoogleAuthCredential({required this.idToken, this.refreshToken = ''}); + + final String idToken; + final String refreshToken; +} diff --git a/lib/domain/repositories/user_repository.dart b/lib/domain/repositories/user_repository.dart index 9c7a5ce6..db7b248c 100644 --- a/lib/domain/repositories/user_repository.dart +++ b/lib/domain/repositories/user_repository.dart @@ -1,17 +1,9 @@ -import 'package:google_sign_in/google_sign_in.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; abstract interface class UserRepository { Stream get userStream; - Stream get googleAuthenticationEvents; - - Future initializeGoogleSignIn(); - - bool get supportsGoogleAuthenticate; - - Future authenticateWithGoogle(); - Future signUp({ required String email, required String password, @@ -22,7 +14,7 @@ abstract interface class UserRepository { Future signOut(); - Future signInWithGoogle(GoogleSignInAccount account); + Future signInWithGoogle(GoogleAuthCredential credential); Future signInWithApple({ required String idToken, diff --git a/lib/presentation/login/components/google_sign_in_button/google_sign_in_button_web.dart b/lib/presentation/login/components/google_sign_in_button/google_sign_in_button_web.dart index f40fdb67..63cacc76 100644 --- a/lib/presentation/login/components/google_sign_in_button/google_sign_in_button_web.dart +++ b/lib/presentation/login/components/google_sign_in_button/google_sign_in_button_web.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in_web/web_only.dart'; import 'package:on_time_front/core/di/di_setup.dart'; +import 'package:on_time_front/core/services/google_authentication_service.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/repositories/user_repository.dart'; class GoogleSignInButton extends StatefulWidget { @@ -16,26 +17,27 @@ class GoogleSignInButton extends StatefulWidget { } class _GoogleSignInButtonState extends State { - final authenticationRepository = getIt.get(); - late final Stream _authenticationEvents; - StreamSubscription? - _authenticationEventsSubscription; + final googleAuthenticationService = getIt.get(); + late final Stream _authenticationCredentials; + StreamSubscription? + _authenticationCredentialsSubscription; @override void initState() { - _authenticationEvents = authenticationRepository.googleAuthenticationEvents; - unawaited(authenticationRepository.initializeGoogleSignIn()); - _authenticationEventsSubscription = _authenticationEvents.listen((event) { - if (event is GoogleSignInAuthenticationEventSignIn) { - unawaited(getIt.get().signInWithGoogle(event.user)); - } + _authenticationCredentials = + googleAuthenticationService.authenticationCredentials; + unawaited(googleAuthenticationService.initialize()); + _authenticationCredentialsSubscription = _authenticationCredentials.listen(( + credential, + ) { + unawaited(getIt.get().signInWithGoogle(credential)); }); super.initState(); } @override void dispose() { - unawaited(_authenticationEventsSubscription?.cancel()); + unawaited(_authenticationCredentialsSubscription?.cancel()); super.dispose(); } diff --git a/lib/presentation/login/screens/sign_in_main_screen.dart b/lib/presentation/login/screens/sign_in_main_screen.dart index 367e8a40..31e14ee9 100644 --- a/lib/presentation/login/screens/sign_in_main_screen.dart +++ b/lib/presentation/login/screens/sign_in_main_screen.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter/foundation.dart'; import 'dart:io' show Platform; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/core/logging/app_logger.dart'; +import 'package:on_time_front/core/services/google_authentication_service.dart'; import 'package:on_time_front/domain/repositories/user_repository.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart'; @@ -105,8 +105,10 @@ class _SignInMainScreenState extends State { Future _defaultGoogleSignIn() async { final userRepository = getIt.get(); - final googleAccount = await userRepository.authenticateWithGoogle(); - await userRepository.signInWithGoogle(googleAccount); + final credential = await getIt + .get() + .authenticate(); + await userRepository.signInWithGoogle(credential); } Future _defaultAppleSignIn() async { @@ -137,8 +139,7 @@ class _SignInMainScreenState extends State { } bool _isUserCancellation(Object error) { - return error is GoogleSignInException && - error.code == GoogleSignInExceptionCode.canceled || + return error is GoogleAuthenticationCanceledException || error is SignInWithAppleAuthorizationException && error.code == AuthorizationErrorCode.canceled; } diff --git a/test/core/dio/interceptors/token_interceptor_test.dart b/test/core/dio/interceptors/token_interceptor_test.dart index 33148b45..6aaf9c48 100644 --- a/test/core/dio/interceptors/token_interceptor_test.dart +++ b/test/core/dio/interceptors/token_interceptor_test.dart @@ -4,10 +4,10 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:on_time_front/core/constants/endpoint.dart'; import 'package:on_time_front/core/dio/interceptors/token_interceptor.dart'; import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/token_entity.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; import 'package:on_time_front/domain/repositories/user_repository.dart'; @@ -310,20 +310,6 @@ class _FakeUserRepository implements UserRepository { @override Stream get userStream => const Stream.empty(); - @override - Stream get googleAuthenticationEvents => - const Stream.empty(); - - @override - bool get supportsGoogleAuthenticate => false; - - @override - Future authenticateWithGoogle() => - throw UnimplementedError(); - - @override - Future initializeGoogleSignIn() async {} - @override Future deleteAppleUser({String? feedbackMessage}) => throw UnimplementedError(); @@ -361,7 +347,7 @@ class _FakeUserRepository implements UserRepository { }) => throw UnimplementedError(); @override - Future signInWithGoogle(GoogleSignInAccount account) => + Future signInWithGoogle(GoogleAuthCredential credential) => throw UnimplementedError(); @override diff --git a/test/data/repositories/user_repository_impl_test.dart b/test/data/repositories/user_repository_impl_test.dart index 2e0c6932..8cd11dc3 100644 --- a/test/data/repositories/user_repository_impl_test.dart +++ b/test/data/repositories/user_repository_impl_test.dart @@ -1,23 +1,30 @@ import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; +import 'package:on_time_front/core/services/google_authentication_service.dart'; import 'package:on_time_front/data/data_sources/authentication_remote_data_source.dart'; import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; import 'package:on_time_front/data/models/sign_in_with_apple_request_model.dart'; import 'package:on_time_front/data/models/sign_in_with_google_request_model.dart'; import 'package:on_time_front/data/repositories/user_repository_impl.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/token_entity.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; void main() { late _FakeAuthenticationRemoteDataSource remoteDataSource; late _FakeTokenLocalDataSource tokenLocalDataSource; + late _FakeGoogleAuthenticationService googleAuthenticationService; late UserRepositoryImpl repository; setUp(() { remoteDataSource = _FakeAuthenticationRemoteDataSource(); tokenLocalDataSource = _FakeTokenLocalDataSource(); - repository = UserRepositoryImpl(remoteDataSource, tokenLocalDataSource); + googleAuthenticationService = _FakeGoogleAuthenticationService(); + repository = UserRepositoryImpl( + remoteDataSource, + tokenLocalDataSource, + googleAuthenticationService, + ); }); test('signIn stores backend tokens and publishes signed-in user', () async { @@ -197,7 +204,7 @@ void main() { remoteDataSource.authResult = (googleUser, _token); await repository.signInWithGoogle( - _FakeGoogleSignInAccount(idToken: 'google-id-token'), + const GoogleAuthCredential(idToken: 'google-id-token'), ); expect(tokenLocalDataSource.deleteCount, 1); @@ -208,9 +215,9 @@ void main() { }, ); - test('signInWithGoogle rejects accounts without an ID token', () async { + test('signInWithGoogle rejects credentials without an ID token', () async { await expectLater( - repository.signInWithGoogle(_FakeGoogleSignInAccount()), + repository.signInWithGoogle(const GoogleAuthCredential(idToken: '')), throwsException, ); @@ -277,8 +284,14 @@ void main() { await expectLater(repository.deleteAppleUser(), throwsException); }); - test('disconnectGoogleSignIn absorbs plugin failures', () async { + test('disconnectGoogleSignIn absorbs provider adapter failures', () async { + googleAuthenticationService.disconnectHandler = () async { + throw Exception('disconnect failed'); + }; + await repository.disconnectGoogleSignIn(); + + expect(googleAuthenticationService.disconnectCount, 1); }); } @@ -399,20 +412,6 @@ class _FakeAuthenticationRemoteDataSource } } -class _FakeGoogleSignInAccount implements GoogleSignInAccount { - _FakeGoogleSignInAccount({this.idToken}); - - final String? idToken; - - @override - GoogleSignInAuthentication get authentication { - return GoogleSignInAuthentication(idToken: idToken); - } - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - class _FakeTokenLocalDataSource implements TokenLocalDataSource { final storedTokens = []; final storedAuthTokens = []; @@ -438,3 +437,27 @@ class _FakeTokenLocalDataSource implements TokenLocalDataSource { deleteCount += 1; } } + +class _FakeGoogleAuthenticationService implements GoogleAuthenticationService { + Future Function()? disconnectHandler; + int disconnectCount = 0; + + @override + Stream get authenticationCredentials => + const Stream.empty(); + + @override + bool get supportsAuthenticate => true; + + @override + Future authenticate() => throw UnimplementedError(); + + @override + Future disconnect() async { + disconnectCount += 1; + await disconnectHandler?.call(); + } + + @override + Future initialize() async {} +} diff --git a/test/domain/repositories/user_repository_boundary_test.dart b/test/domain/repositories/user_repository_boundary_test.dart new file mode 100644 index 00000000..e781a4bf --- /dev/null +++ b/test/domain/repositories/user_repository_boundary_test.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('UserRepository exposes app-owned authentication contract types', () { + final source = File( + 'lib/domain/repositories/user_repository.dart', + ).readAsStringSync(); + + expect( + source, + isNot(contains("package:google_sign_in/google_sign_in.dart")), + ); + expect(source, isNot(matches(RegExp(r'\bGoogleSignIn\w*')))); + }); +} diff --git a/test/domain/use-cases/delete_user_use_case_test.dart b/test/domain/use-cases/delete_user_use_case_test.dart index a203fde9..1ee74c20 100644 --- a/test/domain/use-cases/delete_user_use_case_test.dart +++ b/test/domain/use-cases/delete_user_use_case_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; import 'package:on_time_front/domain/repositories/user_repository.dart'; import 'package:on_time_front/domain/use-cases/delete_user_use_case.dart'; @@ -50,20 +50,6 @@ class _FakeUserRepository implements UserRepository { @override Stream get userStream => const Stream.empty(); - @override - Stream get googleAuthenticationEvents => - const Stream.empty(); - - @override - bool get supportsGoogleAuthenticate => false; - - @override - Future authenticateWithGoogle() => - throw UnimplementedError(); - - @override - Future initializeGoogleSignIn() async {} - @override Future deleteAppleUser({String? feedbackMessage}) async { deletedAppleFeedback = feedbackMessage; @@ -106,7 +92,7 @@ class _FakeUserRepository implements UserRepository { }) => throw UnimplementedError(); @override - Future signInWithGoogle(GoogleSignInAccount account) => + Future signInWithGoogle(GoogleAuthCredential credential) => throw UnimplementedError(); @override diff --git a/test/domain/use-cases/reconcile_alarms_use_case_test.dart b/test/domain/use-cases/reconcile_alarms_use_case_test.dart index 64c7ec68..b8330d4f 100644 --- a/test/domain/use-cases/reconcile_alarms_use_case_test.dart +++ b/test/domain/use-cases/reconcile_alarms_use_case_test.dart @@ -1,8 +1,8 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:on_time_front/core/services/alarm_scheduler_service.dart'; import 'package:on_time_front/core/services/fallback_alarm_notification_service.dart'; import 'package:on_time_front/domain/entities/alarm_entities.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/place_entity.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; @@ -254,20 +254,6 @@ class FakeUserRepository implements UserRepository { @override Stream get userStream => const Stream.empty(); - @override - Stream get googleAuthenticationEvents => - const Stream.empty(); - - @override - bool get supportsGoogleAuthenticate => false; - - @override - Future authenticateWithGoogle() => - throw UnimplementedError(); - - @override - Future initializeGoogleSignIn() async {} - @override Future signOut() async { signedOut = true; @@ -310,7 +296,7 @@ class FakeUserRepository implements UserRepository { }) => throw UnimplementedError(); @override - Future signInWithGoogle(GoogleSignInAccount account) => + Future signInWithGoogle(GoogleAuthCredential credential) => throw UnimplementedError(); @override diff --git a/test/domain/use-cases/schedule_mutation_use_cases_test.dart b/test/domain/use-cases/schedule_mutation_use_cases_test.dart index 88e58807..72204c3e 100644 --- a/test/domain/use-cases/schedule_mutation_use_cases_test.dart +++ b/test/domain/use-cases/schedule_mutation_use_cases_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:on_time_front/domain/entities/alarm_entities.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/place_entity.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; @@ -288,20 +288,6 @@ class _FakeUserRepository implements UserRepository { }) async {} @override - Future signInWithGoogle(GoogleSignInAccount account) async {} - - @override - Future initializeGoogleSignIn() async {} - - @override - bool get supportsGoogleAuthenticate => false; - - @override - Stream get googleAuthenticationEvents => - const Stream.empty(); - @override - Future authenticateWithGoogle() async { - throw UnimplementedError(); - } + Future signInWithGoogle(GoogleAuthCredential credential) async {} } diff --git a/test/domain/use-cases/user_use_cases_test.dart b/test/domain/use-cases/user_use_cases_test.dart index 82b3d3d6..e6d46fdf 100644 --- a/test/domain/use-cases/user_use_cases_test.dart +++ b/test/domain/use-cases/user_use_cases_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; @@ -65,25 +65,12 @@ class _FakeUserRepository implements UserRepository { } @override - Stream get googleAuthenticationEvents => - const Stream.empty(); - - @override - bool get supportsGoogleAuthenticate => false; - - @override - Future authenticateWithGoogle() => - throw UnimplementedError(); - @override Future getUser() async { getUserCount += 1; events.add('getUser'); } - @override - Future initializeGoogleSignIn() async {} - @override Future deleteAppleUser({String? feedbackMessage}) => throw UnimplementedError(); @@ -118,7 +105,7 @@ class _FakeUserRepository implements UserRepository { }) => throw UnimplementedError(); @override - Future signInWithGoogle(GoogleSignInAccount account) => + Future signInWithGoogle(GoogleAuthCredential credential) => throw UnimplementedError(); @override diff --git a/test/presentation/login/screens/sign_in_main_screen_test.dart b/test/presentation/login/screens/sign_in_main_screen_test.dart index f66fca0f..d6f1e701 100644 --- a/test/presentation/login/screens/sign_in_main_screen_test.dart +++ b/test/presentation/login/screens/sign_in_main_screen_test.dart @@ -2,13 +2,25 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; +import 'package:on_time_front/core/di/di_setup.dart'; +import 'package:on_time_front/core/services/google_authentication_service.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; +import 'package:on_time_front/domain/entities/user_entity.dart'; +import 'package:on_time_front/domain/repositories/user_repository.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/login/screens/sign_in_main_screen.dart'; import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart'; import 'package:on_time_front/presentation/shared/theme/theme.dart'; void main() { + setUp(() async { + await getIt.reset(); + }); + + tearDown(() async { + await getIt.reset(); + }); + testWidgets( 'social sign-in buttons stay visible and ignore taps while session is pending', (tester) async { @@ -70,9 +82,8 @@ void main() { (tester) async { await _pumpSubject( tester, - onGoogleSignIn: () async => throw const GoogleSignInException( - code: GoogleSignInExceptionCode.canceled, - ), + onGoogleSignIn: () async => + throw const GoogleAuthenticationCanceledException(), ); await tester.tap(find.text('Sign in with Google')); @@ -82,6 +93,30 @@ void main() { expect(find.text('Sign in with Google'), findsOneWidget); }, ); + + testWidgets( + 'default Google sign-in establishes OnTime session with provider credential', + (tester) async { + const credential = GoogleAuthCredential(idToken: 'google-id-token'); + final googleAuthenticationService = _FakeGoogleAuthenticationService( + credential, + ); + final userRepository = _FakeUserRepository(); + getIt.registerSingleton( + googleAuthenticationService, + ); + getIt.registerSingleton(userRepository); + + await _pumpSubject(tester); + + await tester.tap(find.text('Sign in with Google')); + await tester.pumpAndSettle(); + + expect(googleAuthenticationService.authenticateCount, 1); + expect(userRepository.googleCredentials, [credential]); + expect(find.text('로그인에 실패했어요'), findsNothing); + }, + ); } Future _pumpSubject( @@ -98,3 +133,44 @@ Future _pumpSubject( ), ); } + +class _FakeGoogleAuthenticationService implements GoogleAuthenticationService { + _FakeGoogleAuthenticationService(this.credential); + + final GoogleAuthCredential credential; + int authenticateCount = 0; + + @override + Stream get authenticationCredentials => + const Stream.empty(); + + @override + bool get supportsAuthenticate => true; + + @override + Future authenticate() async { + authenticateCount += 1; + return credential; + } + + @override + Future disconnect() async {} + + @override + Future initialize() async {} +} + +class _FakeUserRepository implements UserRepository { + final googleCredentials = []; + + @override + Stream get userStream => const Stream.empty(); + + @override + Future signInWithGoogle(GoogleAuthCredential credential) async { + googleCredentials.add(credential); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/presentation/my_page/delete_user_modal_test.dart b/test/presentation/my_page/delete_user_modal_test.dart index 029911eb..d0c1c7f1 100644 --- a/test/presentation/my_page/delete_user_modal_test.dart +++ b/test/presentation/my_page/delete_user_modal_test.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; +import 'package:on_time_front/domain/entities/google_auth_credential.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; import 'package:on_time_front/domain/repositories/user_repository.dart'; import 'package:on_time_front/domain/use-cases/delete_user_use_case.dart'; @@ -232,20 +232,6 @@ class _FakeUserRepository implements UserRepository { @override Stream get userStream => const Stream.empty(); - @override - Stream get googleAuthenticationEvents => - const Stream.empty(); - - @override - bool get supportsGoogleAuthenticate => false; - - @override - Future authenticateWithGoogle() => - throw UnimplementedError(); - - @override - Future initializeGoogleSignIn() async {} - @override Future deleteAppleUser({String? feedbackMessage}) => throw UnimplementedError(); @@ -290,7 +276,7 @@ class _FakeUserRepository implements UserRepository { }) => throw UnimplementedError(); @override - Future signInWithGoogle(GoogleSignInAccount account) => + Future signInWithGoogle(GoogleAuthCredential credential) => throw UnimplementedError(); @override