From d9f1dddf45a2eeff25512ba6980ba0a4daa25865 Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:46:57 +0100 Subject: [PATCH] refactor authentication (#552) --- lib/core/storage/secure_storage.dart | 65 ------ .../authentication_local_data_source.dart | 64 ++++++ .../authentication_interceptor.dart | 16 +- .../data/models/authenticated_user_model.dart | 25 +++ .../domain/entities/authenticated_user.dart | 8 +- .../usecases/clear_authenticated_user.dart | 11 + .../usecases/get_authenticated_user.dart | 13 ++ .../usecases/save_authenticated_user.dart | 22 ++ .../cubits/authentication_cubit.dart | 44 ++-- .../account_remote_data_source.dart | 8 +- .../data/reactivation_authenticator.dart | 46 ++-- .../pages/redeem_voucher_page.dart | 2 +- lib/service_locator.dart | 100 +++++---- makefile | 3 - test/core/storage/secure_storage_test.dart | 199 ------------------ test/data/storage/secure_storage_test.dart | 199 ------------------ ...authentication_local_data_source_test.dart | 127 +++++++++++ .../authentication_interceptor_test.dart | 28 ++- .../models/authenticated_user_model_test.dart | 44 ++++ .../cubit/authentication_cubit_test.dart | 111 ++++++---- .../account_remote_data_source_test.dart | 8 +- .../data/reactivation_authenticator_test.dart | 132 +++++++----- .../authenticated_user.json | 5 + test/fixtures/fixture_reader.dart | 5 + 24 files changed, 619 insertions(+), 666 deletions(-) delete mode 100644 lib/core/storage/secure_storage.dart create mode 100644 lib/features/authentication/data/datasources/authentication_local_data_source.dart create mode 100644 lib/features/authentication/data/models/authenticated_user_model.dart create mode 100644 lib/features/authentication/domain/usecases/clear_authenticated_user.dart create mode 100644 lib/features/authentication/domain/usecases/get_authenticated_user.dart create mode 100644 lib/features/authentication/domain/usecases/save_authenticated_user.dart delete mode 100644 test/core/storage/secure_storage_test.dart delete mode 100644 test/data/storage/secure_storage_test.dart create mode 100644 test/features/authentication/data/datasources/authentication_local_data_source_test.dart create mode 100644 test/features/authentication/data/models/authenticated_user_model_test.dart create mode 100644 test/fixtures/authenticated_user/authenticated_user.json create mode 100644 test/fixtures/fixture_reader.dart diff --git a/lib/core/storage/secure_storage.dart b/lib/core/storage/secure_storage.dart deleted file mode 100644 index 6d914911a..000000000 --- a/lib/core/storage/secure_storage.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:coffeecard/features/authentication/domain/entities/authenticated_user.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:logger/logger.dart'; - -class SecureStorage { - static const _emailKey = 'email'; - static const _tokenKey = 'authentication_token'; - static const _encodedPasscodeKey = 'encoded_passcode'; - - final FlutterSecureStorage _storage; - final Logger _logger; - - const SecureStorage({ - required FlutterSecureStorage storage, - required Logger logger, - }) : _storage = storage, - _logger = logger; - - Future saveAuthenticatedUser( - String email, - String encodedPasscode, - String token, - ) async { - await _storage.write(key: _emailKey, value: email); - await _storage.write(key: _encodedPasscodeKey, value: encodedPasscode); - await _storage.write(key: _tokenKey, value: token); - _logger.d( - 'Email ($email), encoded passcode and token added to Secure Storage', - ); - } - - Future getAuthenticatedUser() async { - final email = await readEmail(); - final token = await readToken(); - - return email != null && token != null - ? AuthenticatedUser(email: email, token: token) - : null; - } - - Future clearAuthenticatedUser() async { - if (await getAuthenticatedUser() == null) return; - await _storage.delete(key: _emailKey); - await _storage.delete(key: _encodedPasscodeKey); - await _storage.delete(key: _tokenKey); - _logger.d('Email, encoded passcode and token removed from Secure Storage'); - } - - Future updateToken(String token) async { - await _storage.write(key: _tokenKey, value: token); - _logger.d('Token updated in Secure Storage'); - } - - Future readEmail() async { - return _storage.read(key: _emailKey); - } - - Future readEncodedPasscode() async { - return _storage.read(key: _encodedPasscodeKey); - } - - Future readToken() async { - return _storage.read(key: _tokenKey); - } -} diff --git a/lib/features/authentication/data/datasources/authentication_local_data_source.dart b/lib/features/authentication/data/datasources/authentication_local_data_source.dart new file mode 100644 index 000000000..e7a27a334 --- /dev/null +++ b/lib/features/authentication/data/datasources/authentication_local_data_source.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; + +import 'package:coffeecard/features/authentication/data/models/authenticated_user_model.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:logger/logger.dart'; + +class AuthenticationLocalDataSource { + static const _authenticatedUserKey = 'authenticated_user'; + + final FlutterSecureStorage storage; + final Logger logger; + + const AuthenticationLocalDataSource({ + required this.storage, + required this.logger, + }); + + Future saveAuthenticatedUser( + AuthenticatedUserModel authenticatedUser, + ) async { + await storage.write( + key: _authenticatedUserKey, + value: json.encode(authenticatedUser), + ); + + logger.d('$authenticatedUser added to storage'); + } + + Future> getAuthenticatedUser() async { + final jsonString = await storage.read(key: _authenticatedUserKey); + + if (jsonString == null) { + return const None(); + } + + final user = AuthenticatedUserModel.fromJson( + json.decode(jsonString) as Map, + ); + + return Some(user); + } + + Future clearAuthenticatedUser() async { + await storage.delete(key: _authenticatedUserKey); + logger.d('deleted data for $_authenticatedUserKey'); + } + + Future updateToken(String token) async { + final user = await getAuthenticatedUser(); + + user.map( + (user) async { + final model = AuthenticatedUserModel( + email: user.email, + token: token, + encodedPasscode: user.encodedPasscode, + ); + + await saveAuthenticatedUser(model); + }, + ); + } +} diff --git a/lib/features/authentication/data/intercepters/authentication_interceptor.dart b/lib/features/authentication/data/intercepters/authentication_interceptor.dart index d006ab9fc..bd5bda8ba 100644 --- a/lib/features/authentication/data/intercepters/authentication_interceptor.dart +++ b/lib/features/authentication/data/intercepters/authentication_interceptor.dart @@ -1,25 +1,23 @@ import 'dart:async'; import 'package:chopper/chopper.dart'; -import 'package:coffeecard/core/storage/secure_storage.dart'; +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; class AuthenticationInterceptor implements RequestInterceptor { - final SecureStorage _storage; + final AuthenticationLocalDataSource localDataSource; - AuthenticationInterceptor(this._storage); + AuthenticationInterceptor(this.localDataSource); /// Try retrieve authentication token from storage and add authentication header if exists @override FutureOr onRequest(Request request) async { - final token = await _storage.readToken(); + final user = await localDataSource.getAuthenticatedUser(); - if (token != null) { + return user.match(() => request, (user) { final updatedHeaders = Map.of(request.headers); - updatedHeaders['Authorization'] = 'Bearer $token'; + updatedHeaders['Authorization'] = 'Bearer ${user.token}'; return request.copyWith(headers: updatedHeaders); - } - - return request; + }); } } diff --git a/lib/features/authentication/data/models/authenticated_user_model.dart b/lib/features/authentication/data/models/authenticated_user_model.dart new file mode 100644 index 000000000..f7f257976 --- /dev/null +++ b/lib/features/authentication/data/models/authenticated_user_model.dart @@ -0,0 +1,25 @@ +import 'package:coffeecard/features/authentication/domain/entities/authenticated_user.dart'; + +class AuthenticatedUserModel extends AuthenticatedUser { + const AuthenticatedUserModel({ + required super.email, + required super.token, + required super.encodedPasscode, + }); + + factory AuthenticatedUserModel.fromJson(Map json) { + return AuthenticatedUserModel( + email: json['email'] as String, + token: json['token'] as String, + encodedPasscode: json['passcode'] as String, + ); + } + + Map toJson() { + return { + 'email': email, + 'token': token, + 'passcode': encodedPasscode, + }; + } +} diff --git a/lib/features/authentication/domain/entities/authenticated_user.dart b/lib/features/authentication/domain/entities/authenticated_user.dart index 1939393c3..a9b0ed931 100644 --- a/lib/features/authentication/domain/entities/authenticated_user.dart +++ b/lib/features/authentication/domain/entities/authenticated_user.dart @@ -3,12 +3,18 @@ import 'package:equatable/equatable.dart'; class AuthenticatedUser extends Equatable { final String email; final String token; + final String encodedPasscode; const AuthenticatedUser({ required this.email, required this.token, + required this.encodedPasscode, }); @override - List get props => [email, token]; + List get props => [ + email, + token, + encodedPasscode, + ]; } diff --git a/lib/features/authentication/domain/usecases/clear_authenticated_user.dart b/lib/features/authentication/domain/usecases/clear_authenticated_user.dart new file mode 100644 index 000000000..8ed4239ab --- /dev/null +++ b/lib/features/authentication/domain/usecases/clear_authenticated_user.dart @@ -0,0 +1,11 @@ +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; + +class ClearAuthenticatedUser { + final AuthenticationLocalDataSource dataSource; + + ClearAuthenticatedUser({required this.dataSource}); + + Future call() async { + await dataSource.clearAuthenticatedUser(); + } +} diff --git a/lib/features/authentication/domain/usecases/get_authenticated_user.dart b/lib/features/authentication/domain/usecases/get_authenticated_user.dart new file mode 100644 index 000000000..3bb1911b3 --- /dev/null +++ b/lib/features/authentication/domain/usecases/get_authenticated_user.dart @@ -0,0 +1,13 @@ +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; +import 'package:coffeecard/features/authentication/domain/entities/authenticated_user.dart'; +import 'package:fpdart/fpdart.dart'; + +class GetAuthenticatedUser { + final AuthenticationLocalDataSource dataSource; + + GetAuthenticatedUser({required this.dataSource}); + + Future> call() async { + return dataSource.getAuthenticatedUser(); + } +} diff --git a/lib/features/authentication/domain/usecases/save_authenticated_user.dart b/lib/features/authentication/domain/usecases/save_authenticated_user.dart new file mode 100644 index 000000000..7cb3e71a5 --- /dev/null +++ b/lib/features/authentication/domain/usecases/save_authenticated_user.dart @@ -0,0 +1,22 @@ +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; +import 'package:coffeecard/features/authentication/data/models/authenticated_user_model.dart'; + +class SaveAuthenticatedUser { + final AuthenticationLocalDataSource dataSource; + + SaveAuthenticatedUser({required this.dataSource}); + + Future call({ + required String email, + required String token, + required String encodedPasscode, + }) async { + return dataSource.saveAuthenticatedUser( + AuthenticatedUserModel( + email: email, + token: token, + encodedPasscode: encodedPasscode, + ), + ); + } +} diff --git a/lib/features/authentication/presentation/cubits/authentication_cubit.dart b/lib/features/authentication/presentation/cubits/authentication_cubit.dart index da06eee26..d12fe7e04 100644 --- a/lib/features/authentication/presentation/cubits/authentication_cubit.dart +++ b/lib/features/authentication/presentation/cubits/authentication_cubit.dart @@ -1,5 +1,7 @@ -import 'package:coffeecard/core/storage/secure_storage.dart'; import 'package:coffeecard/features/authentication/domain/entities/authenticated_user.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/clear_authenticated_user.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/get_authenticated_user.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/save_authenticated_user.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,17 +11,24 @@ part 'authentication_state.dart'; // trigger a logout (for instance, when the user requests logs out themselves // vs the user's token expires and fails to renew). class AuthenticationCubit extends Cubit { - AuthenticationCubit(this._storage) : super(const AuthenticationState._()); + final ClearAuthenticatedUser clearAuthenticatedUser; + final SaveAuthenticatedUser saveAuthenticatedUser; + final GetAuthenticatedUser getAuthenticatedUser; - final SecureStorage _storage; + AuthenticationCubit({ + required this.clearAuthenticatedUser, + required this.saveAuthenticatedUser, + required this.getAuthenticatedUser, + }) : super(const AuthenticationState._()); Future appStarted() async { - final authenticatedUser = await _storage.getAuthenticatedUser(); - if (authenticatedUser != null) { - emit(AuthenticationState.authenticated(authenticatedUser)); - } else { - emit(const AuthenticationState.unauthenticated()); - } + final authenticatedUser = await getAuthenticatedUser(); + + authenticatedUser.match( + () => emit(const AuthenticationState.unauthenticated()), + (authenticatedUser) => + emit(AuthenticationState.authenticated(authenticatedUser)), + ); } Future authenticated( @@ -27,20 +36,25 @@ class AuthenticationCubit extends Cubit { String encodedPasscode, String token, ) async { - await _storage.saveAuthenticatedUser( - email, - encodedPasscode, - token, + await saveAuthenticatedUser( + email: email, + token: token, + encodedPasscode: encodedPasscode, ); + emit( AuthenticationState.authenticated( - AuthenticatedUser(token: token, email: email), + AuthenticatedUser( + token: token, + email: email, + encodedPasscode: encodedPasscode, + ), ), ); } Future unauthenticated() async { - await _storage.clearAuthenticatedUser(); + await clearAuthenticatedUser(); emit(const AuthenticationState.unauthenticated()); } } diff --git a/lib/features/login/data/datasources/account_remote_data_source.dart b/lib/features/login/data/datasources/account_remote_data_source.dart index cc8be0315..3b08b0a84 100644 --- a/lib/features/login/data/datasources/account_remote_data_source.dart +++ b/lib/features/login/data/datasources/account_remote_data_source.dart @@ -44,7 +44,13 @@ class AccountRemoteDataSource { return Left(err); }, - (result) => Right(AuthenticatedUser(email: email, token: result.token!)), + (result) => Right( + AuthenticatedUser( + email: email, + encodedPasscode: encodedPasscode, + token: result.token!, + ), + ), ); } diff --git a/lib/features/reactivation/data/reactivation_authenticator.dart b/lib/features/reactivation/data/reactivation_authenticator.dart index 6da674188..91866886d 100644 --- a/lib/features/reactivation/data/reactivation_authenticator.dart +++ b/lib/features/reactivation/data/reactivation_authenticator.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:chopper/chopper.dart'; -import 'package:coffeecard/core/storage/secure_storage.dart'; +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; import 'package:coffeecard/features/authentication/presentation/cubits/authentication_cubit.dart'; import 'package:coffeecard/features/login/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/features/reactivation/data/throttler.dart'; @@ -16,7 +16,7 @@ class ReactivationAuthenticator extends Authenticator { bool _ready = false; late final AccountRemoteDataSource _accountRemoteDataSource; - final SecureStorage _secureStorage; + final AuthenticationLocalDataSource _authenticationLocalDataSource; final AuthenticationCubit _authenticationCubit; final Logger _logger; @@ -26,9 +26,9 @@ class ReactivationAuthenticator extends Authenticator { /// /// This instance is not ready to be used. Call [initialize] before using it. ReactivationAuthenticator.uninitialized({required GetIt serviceLocator}) - : _secureStorage = serviceLocator(), - _authenticationCubit = serviceLocator(), - _logger = serviceLocator(); + : _authenticationLocalDataSource = serviceLocator(), + _authenticationCubit = serviceLocator(), + _logger = serviceLocator(); /// Initializes the [ReactivationAuthenticator] by providing the /// [AccountRemoteDataSource] to use. @@ -92,25 +92,29 @@ class ReactivationAuthenticator extends Authenticator { return Task( () async { // Check if user credentials are stored; if not, return None. - final email = await _secureStorage.readEmail(); - final encodedPasscode = await _secureStorage.readEncodedPasscode(); - if (email == null || encodedPasscode == null) { - return none(); - } - - // Attempt to log in with the stored credentials. - // This login call may return 401 if the stored credentials are invalid; - // recursive calls to [authenticate] are blocked by a check in the - // [authenticate] method. - final either = - await _accountRemoteDataSource.login(email, encodedPasscode); - - return Option.fromEither(either).map((user) => user.token); + final user = + await _authenticationLocalDataSource.getAuthenticatedUser(); + + return user.match( + () => none(), + (user) async { + // Attempt to log in with the stored credentials. + // This login call may return 401 if the stored credentials are invalid; + // recursive calls to [authenticate] are blocked by a check in the + // [authenticate] method. + final either = await _accountRemoteDataSource.login( + user.email, + user.encodedPasscode, + ); + + return Option.fromEither(either).map((user) => user.token); + }, + ); }, ); } - /// Saves the [token] in [SecureStorage] + /// Saves the [token] in [AuthenticationLocalDataSource] /// or signs out the user if the [token] is [None]. Task _saveOrEvict(Option token) { return Task(() async { @@ -122,7 +126,7 @@ class ReactivationAuthenticator extends Authenticator { }, (token) async { _logRefreshTokenSucceeded(); - await _secureStorage.updateToken(token); + await _authenticationLocalDataSource.updateToken(token); return unit; }, ); diff --git a/lib/features/voucher/presentation/pages/redeem_voucher_page.dart b/lib/features/voucher/presentation/pages/redeem_voucher_page.dart index 0fef7d85d..342368af3 100644 --- a/lib/features/voucher/presentation/pages/redeem_voucher_page.dart +++ b/lib/features/voucher/presentation/pages/redeem_voucher_page.dart @@ -29,7 +29,7 @@ class RedeemVoucherPage extends StatelessWidget { final _ = LoadingOverlay.show(context); return; } - LoadingOverlay.hide(context); + final _ = LoadingOverlay.hide(context); if (state is VoucherSuccess) return _onSuccess(context, state); if (state is VoucherError) return _onError(context, state); }, diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 44956c7fc..3b1dd6710 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -6,9 +6,12 @@ import 'package:coffeecard/core/external/screen_brightness.dart'; import 'package:coffeecard/core/firebase_analytics_event_logging.dart'; import 'package:coffeecard/core/ignore_value.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; -import 'package:coffeecard/core/storage/secure_storage.dart'; import 'package:coffeecard/env/env.dart'; +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; import 'package:coffeecard/features/authentication/data/intercepters/authentication_interceptor.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/clear_authenticated_user.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/get_authenticated_user.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/save_authenticated_user.dart'; import 'package:coffeecard/features/authentication/presentation/cubits/authentication_cubit.dart'; import 'package:coffeecard/features/contributor/data/datasources/contributor_local_data_source.dart'; import 'package:coffeecard/features/contributor/domain/usecases/fetch_contributors.dart'; @@ -69,65 +72,41 @@ import 'package:logger/logger.dart'; final GetIt sl = GetIt.instance; void configureServices() { - // Logger - ignoreValue( - sl.registerSingleton(Logger()), - ); - - // Executor - sl.registerLazySingleton( - () => NetworkRequestExecutor( - logger: sl(), - firebaseLogger: sl(), - ), - ); - - // Storage - ignoreValue( - sl.registerSingleton( - SecureStorage(storage: const FlutterSecureStorage(), logger: sl()), - ), - ); + ignoreValue(sl.registerSingleton(Logger())); - // Authentication - ignoreValue( - sl.registerSingleton( - AuthenticationCubit(sl()), - ), - ); + initFeatures(); + initExternal(); // Reactivation authenticator (uninitalized), http client and interceptors initHttp(); - // Features - initFeatures(); + // provide the account repository to the reactivation authenticator + sl().initialize(sl()); +} - // v1 and v2 +void initExternal() { + ignoreValue(sl.registerSingleton(const FlutterSecureStorage())); - sl.registerFactory( - () => AccountRemoteDataSource( - apiV1: sl(), - apiV2: sl(), - executor: sl(), - ), - ); + ignoreValue(sl.registerFactory(() => DateService())); + ignoreValue(sl.registerFactory(() => ScreenBrightness())); + ignoreValue(sl.registerLazySingleton(() => ExternalUrlLauncher())); - // external ignoreValue( sl.registerSingleton( FirebaseAnalyticsEventLogging(FirebaseAnalytics.instance), ), ); - ignoreValue(sl.registerFactory(() => DateService())); - ignoreValue(sl.registerFactory(() => ScreenBrightness())); - ignoreValue(sl.registerLazySingleton(() => ExternalUrlLauncher())); - - // provide the account repository to the reactivation authenticator - sl().initialize(sl()); + sl.registerLazySingleton( + () => NetworkRequestExecutor( + logger: sl(), + firebaseLogger: sl(), + ), + ); } void initFeatures() { + initAuthentication(); initOpeningHours(); initOccupation(); initUser(); @@ -143,6 +122,32 @@ void initFeatures() { initRegister(); } +void initAuthentication() { + // bloc + sl.registerLazySingleton( + () => AuthenticationCubit( + clearAuthenticatedUser: sl(), + saveAuthenticatedUser: sl(), + getAuthenticatedUser: sl(), + ), + ); + + // use case + sl.registerFactory(() => ClearAuthenticatedUser(dataSource: sl())); + sl.registerFactory(() => SaveAuthenticatedUser(dataSource: sl())); + sl.registerFactory(() => GetAuthenticatedUser(dataSource: sl())); + + // repository + + // data source + sl.registerLazySingleton( + () => AuthenticationLocalDataSource( + storage: sl(), + logger: sl(), + ), + ); +} + void initOpeningHours() { // bloc sl.registerFactory( @@ -339,6 +344,13 @@ void initLogin() { sl.registerFactory(() => ResendEmail(remoteDataSource: sl())); // data source + sl.registerLazySingleton( + () => AccountRemoteDataSource( + apiV1: sl(), + apiV2: sl(), + executor: sl(), + ), + ); } void initRegister() { @@ -368,7 +380,7 @@ void initHttp() { final coffeCardChopper = ChopperClient( baseUrl: Uri.parse(Env.coffeeCardUrl), - interceptors: [AuthenticationInterceptor(sl())], + interceptors: [AuthenticationInterceptor(sl())], converter: $JsonSerializableConverter(), services: [ CoffeecardApi.create(), diff --git a/makefile b/makefile index 82c8dd027..1b0e91d1f 100644 --- a/makefile +++ b/makefile @@ -8,9 +8,6 @@ icon: dart run flutter_launcher_icons:main splash: dart run flutter_native_splash:create -analyze: - flutter analyze && \ - dart run dart_code_metrics:metrics analyze lib update_icon: dart run flutter_launcher_icons:main update_name: diff --git a/test/core/storage/secure_storage_test.dart b/test/core/storage/secure_storage_test.dart deleted file mode 100644 index 98ca47802..000000000 --- a/test/core/storage/secure_storage_test.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'package:coffeecard/core/storage/secure_storage.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:logger/logger.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'secure_storage_test.mocks.dart'; - -@GenerateNiceMocks([MockSpec(), MockSpec()]) -void main() { - const emailKey = 'email'; - const tokenKey = 'authentication_token'; - const encodedPasscodeKey = 'encoded_passcode'; - - late SecureStorage secureStorage; - late MockFlutterSecureStorage mockStorage; - late MockLogger mockLogger; - - setUp(() { - mockStorage = MockFlutterSecureStorage(); - mockLogger = MockLogger(); - secureStorage = SecureStorage(storage: mockStorage, logger: mockLogger); - }); - - test( - 'GIVEN user credentials ' - 'WHEN saveAuthenticatedUser is called ' - 'THEN it should save the user credentials to secure storage', - () async { - const email = 'test@example.com'; - const encodedPasscode = 'encodedPasscode'; - const token = 'token'; - - await secureStorage.saveAuthenticatedUser( - email, - encodedPasscode, - token, - ); - - verifyInOrder([ - mockStorage.write(key: emailKey, value: email), - mockStorage.write(key: encodedPasscodeKey, value: encodedPasscode), - mockStorage.write(key: tokenKey, value: token), - mockLogger.d(any), - ]); - }, - ); - - test( - 'GIVEN user credentials in secure storage ' - 'WHEN getAuthenticatedUser is called ' - 'THEN it should return the authenticated user', - () async { - const email = 'test@example.com'; - const token = 'token'; - - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => email); - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => token); - - final user = await secureStorage.getAuthenticatedUser(); - - expect(user, isNotNull); - expect(user?.email, email); - expect(user?.token, token); - }, - ); - - test( - 'GIVEN missing token in secure storage ' - 'WHEN getAuthenticatedUser is called ' - 'THEN it should return null', - () async { - const email = 'test@example.com'; - - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => email); - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => null); - - final user = await secureStorage.getAuthenticatedUser(); - - expect(user, isNull); - }, - ); - - test( - 'GIVEN user credentials in secure storage ' - 'WHEN clearAuthenticatedUser is called ' - 'THEN it should remove the user credentials from secure storage', - () async { - when(mockStorage.read(key: emailKey)) - .thenAnswer((_) async => 'test@example.com'); - when(mockStorage.read(key: encodedPasscodeKey)) - .thenAnswer((_) async => 'encodedPasscode'); - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => 'token'); - - await secureStorage.clearAuthenticatedUser(); - - verifyInOrder([ - mockStorage.delete(key: emailKey), - mockStorage.delete(key: encodedPasscodeKey), - mockStorage.delete(key: tokenKey), - mockLogger.d(any), - ]); - }, - ); - - test( - 'GIVEN missing email in secure storage ' - 'WHEN clearAuthenticatedUser is called ' - 'THEN it should not remove any user credentials', - () async { - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => null); - - await secureStorage.clearAuthenticatedUser(); - - verifyInOrder([ - mockStorage.read(key: emailKey), - mockStorage.read(key: tokenKey), - ]); - verifyNever(mockStorage.delete(key: anyNamed('key'))); - }, - ); - - test( - 'GIVEN a new token ' - 'WHEN updateToken is called ' - 'THEN it should update the token in secure storage', - () async { - const token = 'new_token'; - - await secureStorage.updateToken(token); - - verify(mockStorage.write(key: tokenKey, value: token)); - verify(mockLogger.d('Token updated in Secure Storage')); - }, - ); - - test( - 'GIVEN email stored in secure storage ' - 'WHEN readEmail is called ' - 'THEN it should return the email', - () async { - const email = 'test@example.com'; - - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => email); - - final result = await secureStorage.readEmail(); - - expect(result, email); - }, - ); - - test( - 'GIVEN encoded passcode stored in secure storage ' - 'WHEN readEncodedPasscode is called ' - 'THEN it should return the encoded passcode', - () async { - const encodedPasscode = 'encodedPasscode'; - - when(mockStorage.read(key: encodedPasscodeKey)) - .thenAnswer((_) async => encodedPasscode); - - final result = await secureStorage.readEncodedPasscode(); - - expect(result, encodedPasscode); - }, - ); - - test( - 'GIVEN token stored in secure storage ' - 'WHEN readToken is called ' - 'THEN it should return the token', - () async { - const token = 'token'; - - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => token); - - final result = await secureStorage.readToken(); - - expect(result, token); - }, - ); - - test( - 'GIVEN no token or email stored in secure storage ' - 'WHEN readToken and readEmail are called ' - 'THEN they should return null', - () async { - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => null); - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => null); - - final email = await secureStorage.readEmail(); - final token = await secureStorage.readToken(); - - expect(email, isNull); - expect(token, isNull); - }, - ); -} diff --git a/test/data/storage/secure_storage_test.dart b/test/data/storage/secure_storage_test.dart deleted file mode 100644 index 98ca47802..000000000 --- a/test/data/storage/secure_storage_test.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'package:coffeecard/core/storage/secure_storage.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:logger/logger.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'secure_storage_test.mocks.dart'; - -@GenerateNiceMocks([MockSpec(), MockSpec()]) -void main() { - const emailKey = 'email'; - const tokenKey = 'authentication_token'; - const encodedPasscodeKey = 'encoded_passcode'; - - late SecureStorage secureStorage; - late MockFlutterSecureStorage mockStorage; - late MockLogger mockLogger; - - setUp(() { - mockStorage = MockFlutterSecureStorage(); - mockLogger = MockLogger(); - secureStorage = SecureStorage(storage: mockStorage, logger: mockLogger); - }); - - test( - 'GIVEN user credentials ' - 'WHEN saveAuthenticatedUser is called ' - 'THEN it should save the user credentials to secure storage', - () async { - const email = 'test@example.com'; - const encodedPasscode = 'encodedPasscode'; - const token = 'token'; - - await secureStorage.saveAuthenticatedUser( - email, - encodedPasscode, - token, - ); - - verifyInOrder([ - mockStorage.write(key: emailKey, value: email), - mockStorage.write(key: encodedPasscodeKey, value: encodedPasscode), - mockStorage.write(key: tokenKey, value: token), - mockLogger.d(any), - ]); - }, - ); - - test( - 'GIVEN user credentials in secure storage ' - 'WHEN getAuthenticatedUser is called ' - 'THEN it should return the authenticated user', - () async { - const email = 'test@example.com'; - const token = 'token'; - - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => email); - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => token); - - final user = await secureStorage.getAuthenticatedUser(); - - expect(user, isNotNull); - expect(user?.email, email); - expect(user?.token, token); - }, - ); - - test( - 'GIVEN missing token in secure storage ' - 'WHEN getAuthenticatedUser is called ' - 'THEN it should return null', - () async { - const email = 'test@example.com'; - - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => email); - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => null); - - final user = await secureStorage.getAuthenticatedUser(); - - expect(user, isNull); - }, - ); - - test( - 'GIVEN user credentials in secure storage ' - 'WHEN clearAuthenticatedUser is called ' - 'THEN it should remove the user credentials from secure storage', - () async { - when(mockStorage.read(key: emailKey)) - .thenAnswer((_) async => 'test@example.com'); - when(mockStorage.read(key: encodedPasscodeKey)) - .thenAnswer((_) async => 'encodedPasscode'); - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => 'token'); - - await secureStorage.clearAuthenticatedUser(); - - verifyInOrder([ - mockStorage.delete(key: emailKey), - mockStorage.delete(key: encodedPasscodeKey), - mockStorage.delete(key: tokenKey), - mockLogger.d(any), - ]); - }, - ); - - test( - 'GIVEN missing email in secure storage ' - 'WHEN clearAuthenticatedUser is called ' - 'THEN it should not remove any user credentials', - () async { - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => null); - - await secureStorage.clearAuthenticatedUser(); - - verifyInOrder([ - mockStorage.read(key: emailKey), - mockStorage.read(key: tokenKey), - ]); - verifyNever(mockStorage.delete(key: anyNamed('key'))); - }, - ); - - test( - 'GIVEN a new token ' - 'WHEN updateToken is called ' - 'THEN it should update the token in secure storage', - () async { - const token = 'new_token'; - - await secureStorage.updateToken(token); - - verify(mockStorage.write(key: tokenKey, value: token)); - verify(mockLogger.d('Token updated in Secure Storage')); - }, - ); - - test( - 'GIVEN email stored in secure storage ' - 'WHEN readEmail is called ' - 'THEN it should return the email', - () async { - const email = 'test@example.com'; - - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => email); - - final result = await secureStorage.readEmail(); - - expect(result, email); - }, - ); - - test( - 'GIVEN encoded passcode stored in secure storage ' - 'WHEN readEncodedPasscode is called ' - 'THEN it should return the encoded passcode', - () async { - const encodedPasscode = 'encodedPasscode'; - - when(mockStorage.read(key: encodedPasscodeKey)) - .thenAnswer((_) async => encodedPasscode); - - final result = await secureStorage.readEncodedPasscode(); - - expect(result, encodedPasscode); - }, - ); - - test( - 'GIVEN token stored in secure storage ' - 'WHEN readToken is called ' - 'THEN it should return the token', - () async { - const token = 'token'; - - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => token); - - final result = await secureStorage.readToken(); - - expect(result, token); - }, - ); - - test( - 'GIVEN no token or email stored in secure storage ' - 'WHEN readToken and readEmail are called ' - 'THEN they should return null', - () async { - when(mockStorage.read(key: emailKey)).thenAnswer((_) async => null); - when(mockStorage.read(key: tokenKey)).thenAnswer((_) async => null); - - final email = await secureStorage.readEmail(); - final token = await secureStorage.readToken(); - - expect(email, isNull); - expect(token, isNull); - }, - ); -} diff --git a/test/features/authentication/data/datasources/authentication_local_data_source_test.dart b/test/features/authentication/data/datasources/authentication_local_data_source_test.dart new file mode 100644 index 000000000..a5796bb66 --- /dev/null +++ b/test/features/authentication/data/datasources/authentication_local_data_source_test.dart @@ -0,0 +1,127 @@ +import 'dart:convert'; + +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; +import 'package:coffeecard/features/authentication/data/models/authenticated_user_model.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:logger/logger.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'authentication_local_data_source_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + late AuthenticationLocalDataSource dataSource; + late MockFlutterSecureStorage storage; + late MockLogger logger; + + setUp(() { + storage = MockFlutterSecureStorage(); + logger = MockLogger(); + dataSource = + AuthenticationLocalDataSource(storage: storage, logger: logger); + }); + + const user = AuthenticatedUserModel( + email: 'email', + token: 'token', + encodedPasscode: 'encodedPasscode', + ); + + group('saveAuthenticatedUser', () { + test( + 'should save user object', + () async { + // arrange + + // act + await dataSource.saveAuthenticatedUser(user); + + // assert + final jsonString = json.encode(user); + + verifyInOrder([ + storage.write(key: anyNamed('key'), value: jsonString), + logger.d(any), + ]); + }, + ); + }); + + group('getAuthenticatedUser', () { + test( + 'should return user when storage contains key', + () async { + // arrange + when(storage.read(key: anyNamed('key'))) + .thenAnswer((_) async => json.encode(user)); + + // act + final actual = await dataSource.getAuthenticatedUser(); + + // assert + expect(actual.isSome(), true); + + actual.match(() {}, (actual) { + expect(actual.email, user.email); + expect(actual.token, user.token); + expect(actual.encodedPasscode, user.encodedPasscode); + }); + }, + ); + test( + 'should return none when storage does not contains key', + () async { + // arrange + when(storage.read(key: anyNamed('key'))).thenAnswer((_) async => null); + + // act + final actual = await dataSource.getAuthenticatedUser(); + + // assert + expect(actual, none()); + }, + ); + }); + + group('clearAuthenticatedUser', () { + test( + 'should delete key', + () async { + // act + await dataSource.clearAuthenticatedUser(); + + // assert + verify(storage.delete(key: anyNamed('key'))); + }, + ); + }); + + group('updateToken', () { + test( + 'should update token in storage', + () async { + // arrange + const token = 'new_token'; + when(storage.read(key: anyNamed('key'))) + .thenAnswer((_) async => json.encode(user)); + + // act + await dataSource.updateToken(token); + + // assert + const expected = AuthenticatedUserModel( + email: 'email', + token: token, + encodedPasscode: 'encodedPasscode', + ); + + verify( + storage.write(key: anyNamed('key'), value: json.encode(expected)), + ); + }, + ); + }); +} diff --git a/test/features/authentication/data/intercepters/authentication_interceptor_test.dart b/test/features/authentication/data/intercepters/authentication_interceptor_test.dart index be3db3133..c6cd76848 100644 --- a/test/features/authentication/data/intercepters/authentication_interceptor_test.dart +++ b/test/features/authentication/data/intercepters/authentication_interceptor_test.dart @@ -1,23 +1,36 @@ import 'package:chopper/chopper.dart'; -import 'package:coffeecard/core/storage/secure_storage.dart'; +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; import 'package:coffeecard/features/authentication/data/intercepters/authentication_interceptor.dart'; +import 'package:coffeecard/features/authentication/data/models/authenticated_user_model.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'authentication_interceptor_test.mocks.dart'; -@GenerateMocks([SecureStorage]) +@GenerateMocks([AuthenticationLocalDataSource]) void main() { + setUp(() { + provideDummy>(none()); + }); + test( 'GIVEN a token in SecureStorage WHEN calling onRequest THEN Authorization Header is added to the request', () async { const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; - final mockSecureStorage = MockSecureStorage(); - when(mockSecureStorage.readToken()) - .thenAnswer((_) => Future.value(token)); + final mockSecureStorage = MockAuthenticationLocalDataSource(); + when(mockSecureStorage.getAuthenticatedUser()).thenAnswer( + (_) async => const Some( + AuthenticatedUserModel( + email: 'email', + token: token, + encodedPasscode: 'encodedPasscode', + ), + ), + ); final interceptor = AuthenticationInterceptor(mockSecureStorage); final request = Request('POST', Uri.parse('url'), Uri.parse('baseurl')); @@ -32,8 +45,9 @@ void main() { test( 'GIVEN no token in SecureStorage WHEN calling onRequest THEN no Authorization Header is added to the request', () async { - final mockSecureStorage = MockSecureStorage(); - when(mockSecureStorage.readToken()).thenAnswer((_) async => null); + final mockSecureStorage = MockAuthenticationLocalDataSource(); + when(mockSecureStorage.getAuthenticatedUser()) + .thenAnswer((_) async => none()); final interceptor = AuthenticationInterceptor(mockSecureStorage); final request = Request('POST', Uri.parse('url'), Uri.parse('baseurl')); diff --git a/test/features/authentication/data/models/authenticated_user_model_test.dart b/test/features/authentication/data/models/authenticated_user_model_test.dart new file mode 100644 index 000000000..5d4096b6d --- /dev/null +++ b/test/features/authentication/data/models/authenticated_user_model_test.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; + +import 'package:coffeecard/features/authentication/data/models/authenticated_user_model.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../fixtures/fixture_reader.dart'; + +void main() { + const model = AuthenticatedUserModel( + email: 'email', + token: 'token', + encodedPasscode: 'passcode', + ); + + group('fromJson', () { + test('should return model', () { + // arrange + final jsonString = fixture('authenticated_user/authenticated_user.json'); + + // act + final actual = AuthenticatedUserModel.fromJson( + json.decode(jsonString) as Map, + ); + + // assert + expect(actual, model); + }); + }); + group('toJson', () { + test('should return map', () { + // act + final actual = model.toJson(); + + // assert + final expected = { + 'email': 'email', + 'token': 'token', + 'passcode': 'passcode', + }; + + expect(actual, expected); + }); + }); +} diff --git a/test/features/authentication/presentation/cubit/authentication_cubit_test.dart b/test/features/authentication/presentation/cubit/authentication_cubit_test.dart index 59dbb5574..890c486cc 100644 --- a/test/features/authentication/presentation/cubit/authentication_cubit_test.dart +++ b/test/features/authentication/presentation/cubit/authentication_cubit_test.dart @@ -1,75 +1,98 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:coffeecard/core/storage/secure_storage.dart'; import 'package:coffeecard/features/authentication/domain/entities/authenticated_user.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/clear_authenticated_user.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/get_authenticated_user.dart'; +import 'package:coffeecard/features/authentication/domain/usecases/save_authenticated_user.dart'; import 'package:coffeecard/features/authentication/presentation/cubits/authentication_cubit.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'authentication_cubit_test.mocks.dart'; -@GenerateMocks([SecureStorage]) +@GenerateMocks( + [ + GetAuthenticatedUser, + ClearAuthenticatedUser, + SaveAuthenticatedUser, + ], +) void main() { - const dummyUser = AuthenticatedUser(email: 'email', token: 'token'); + late AuthenticationCubit cubit; + late MockGetAuthenticatedUser getAuthenticatedUser; + late MockClearAuthenticatedUser clearAuthenticatedUser; + late MockSaveAuthenticatedUser saveAuthenticatedUser; - group('authentication cubit tests', () { - late AuthenticationCubit authenticationCubit; - final secureStorage = MockSecureStorage(); + setUp(() { + getAuthenticatedUser = MockGetAuthenticatedUser(); + clearAuthenticatedUser = MockClearAuthenticatedUser(); + saveAuthenticatedUser = MockSaveAuthenticatedUser(); + cubit = AuthenticationCubit( + getAuthenticatedUser: getAuthenticatedUser, + clearAuthenticatedUser: clearAuthenticatedUser, + saveAuthenticatedUser: saveAuthenticatedUser, + ); + + provideDummy>(none()); + }); - setUp(() { - authenticationCubit = AuthenticationCubit(secureStorage); - }); + const testUser = AuthenticatedUser( + email: 'email', + token: 'token', + encodedPasscode: 'encodedPasscode', + ); - test('initial state is AuthenticationState.unknown', () { - expect(authenticationCubit.state, const AuthenticationState.unknown()); - }); + test('initial state is AuthenticationState.unknown', () { + expect(cubit.state, const AuthenticationState.unknown()); + }); + group('appStarted', () { blocTest( - 'appStarted emits unauthenticated when no user is stored', - build: () { - when(secureStorage.getAuthenticatedUser()) - .thenAnswer((_) async => null); - return authenticationCubit; - }, - act: (cubit) => cubit.appStarted(), + 'should emit [Unauthenticated] when no user is stored', + build: () => cubit, + setUp: () => when(getAuthenticatedUser()).thenAnswer((_) async => none()), + act: (_) => cubit.appStarted(), expect: () => [const AuthenticationState.unauthenticated()], ); blocTest( - 'appStarted emits authenticated when a user is stored', - build: () { - when(secureStorage.getAuthenticatedUser()) - .thenAnswer((_) async => dummyUser); - return authenticationCubit; - }, - act: (cubit) => cubit.appStarted(), - expect: () => [const AuthenticationState.authenticated(dummyUser)], + 'should emit [Authenticated] when a user is stored', + build: () => cubit, + setUp: () => + when(getAuthenticatedUser()).thenAnswer((_) async => some(testUser)), + act: (_) => cubit.appStarted(), + expect: () => [const AuthenticationState.authenticated(testUser)], ); + }); + group('authenticated', () { blocTest( - 'authenticated emits authenticated and saves the user to storage', - build: () => authenticationCubit, - act: (cubit) => cubit.authenticated('email', 'encodedPasscode', 'token'), - expect: () => [const AuthenticationState.authenticated(dummyUser)], - verify: (cubit) => verify( - secureStorage.saveAuthenticatedUser( - 'email', - 'encodedPasscode', - 'token', + 'should emit [Authenticated] and save the user to storage', + build: () => cubit, + act: (_) => cubit.authenticated( + testUser.email, + testUser.encodedPasscode, + testUser.token, + ), + expect: () => [const AuthenticationState.authenticated(testUser)], + verify: (_) => verify( + saveAuthenticatedUser( + email: testUser.email, + token: testUser.token, + encodedPasscode: testUser.encodedPasscode, ), ), ); + }); + group('unauthenticated', () { blocTest( - 'unauthenticated emits unauthenticated and clears the user from storage', - build: () => authenticationCubit, - act: (cubit) => cubit.unauthenticated(), + 'should emit [Unauthenticated] and clear the user from storage', + build: () => cubit, + act: (_) => cubit.unauthenticated(), expect: () => [const AuthenticationState.unauthenticated()], - verify: (cubit) => verify(secureStorage.clearAuthenticatedUser()), + verify: (_) => verify(clearAuthenticatedUser()), ); - - tearDown(() { - authenticationCubit.close(); - }); }); } diff --git a/test/features/login/data/datasources/account_remote_data_source_test.dart b/test/features/login/data/datasources/account_remote_data_source_test.dart index d6b51c7b3..db7624160 100644 --- a/test/features/login/data/datasources/account_remote_data_source_test.dart +++ b/test/features/login/data/datasources/account_remote_data_source_test.dart @@ -67,7 +67,13 @@ void main() { // assert expect( actual, - const Right(AuthenticatedUser(email: 'email', token: 'token')), + const Right( + AuthenticatedUser( + email: 'email', + token: 'token', + encodedPasscode: 'encodedPasscode', + ), + ), ); }, ); diff --git a/test/features/reactivation/data/reactivation_authenticator_test.dart b/test/features/reactivation/data/reactivation_authenticator_test.dart index 71e02eadb..777271c08 100644 --- a/test/features/reactivation/data/reactivation_authenticator_test.dart +++ b/test/features/reactivation/data/reactivation_authenticator_test.dart @@ -1,6 +1,7 @@ import 'package:chopper/chopper.dart' as chopper; import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/core/storage/secure_storage.dart'; +import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; +import 'package:coffeecard/features/authentication/data/models/authenticated_user_model.dart'; import 'package:coffeecard/features/authentication/domain/entities/authenticated_user.dart'; import 'package:coffeecard/features/authentication/presentation/cubits/authentication_cubit.dart'; import 'package:coffeecard/features/login/data/datasources/account_remote_data_source.dart'; @@ -19,34 +20,36 @@ import 'reactivation_authenticator_test.mocks.dart'; @GenerateMocks([ AuthenticationCubit, AccountRemoteDataSource, - SecureStorage, + AuthenticationLocalDataSource, Logger, ]) void main() { late _FakeGetIt serviceLocator; late MockAuthenticationCubit authenticationCubit; late MockAccountRemoteDataSource accountRemoteDataSource; - late MockSecureStorage secureStorage; + late MockAuthenticationLocalDataSource secureStorage; late ReactivationAuthenticator authenticator; setUp(() { serviceLocator = _FakeGetIt.fromMockedObjects( - mockAuthenticationCubit: MockAuthenticationCubit(), - mockAccountRemoteDataSource: MockAccountRemoteDataSource(), - mockSecureStorage: MockSecureStorage(), + authenticationCubit: MockAuthenticationCubit(), + accountRemoteDataSource: MockAccountRemoteDataSource(), + authenticationLocalDataSource: MockAuthenticationLocalDataSource(), mockLogger: MockLogger(), ); authenticationCubit = serviceLocator.getMock(); accountRemoteDataSource = serviceLocator.getMock(); - secureStorage = serviceLocator.getMock(); + secureStorage = serviceLocator.getMock(); authenticator = ReactivationAuthenticator.uninitialized(serviceLocator: serviceLocator); authenticator.initialize(accountRemoteDataSource); + provideDummy>(none()); + provideDummy>( const Left(ConnectionFailure()), ); @@ -81,8 +84,8 @@ void main() { final request = _requestFromMethod('GET'); final response = _responseFromStatusCode(401); - when(secureStorage.readEmail()).thenAnswer((_) async => null); - when(secureStorage.readEncodedPasscode()).thenAnswer((_) async => null); + when(secureStorage.getAuthenticatedUser()) + .thenAnswer((_) async => none()); // Act final result = await authenticator.authenticate(request, response); @@ -119,14 +122,14 @@ void main() { ), ); - when(secureStorage.readEmail()).thenAnswer( - (_) async => email, - ); - when(secureStorage.readEncodedPasscode()).thenAnswer( - (_) async => encodedPasscode, - ); when(secureStorage.getAuthenticatedUser()).thenAnswer( - (_) async => const AuthenticatedUser(email: email, token: token), + (_) async => some( + const AuthenticatedUserModel( + email: email, + token: token, + encodedPasscode: 'encodedPasscode', + ), + ), ); when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( (_) async { @@ -171,19 +174,23 @@ void main() { const oldToken = 'oldToken'; const newToken = 'newToken'; - when(secureStorage.readEmail()).thenAnswer( - (_) async => email, - ); - when(secureStorage.readEncodedPasscode()).thenAnswer( - (_) async => encodedPasscode, - ); - when(secureStorage.readToken()).thenAnswer( - (_) async => oldToken, + when(secureStorage.getAuthenticatedUser()).thenAnswer( + (_) async => some( + const AuthenticatedUserModel( + email: email, + token: oldToken, + encodedPasscode: encodedPasscode, + ), + ), ); when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( (_) async => right( - const AuthenticatedUser(email: email, token: newToken), + const AuthenticatedUser( + email: email, + token: newToken, + encodedPasscode: 'encodedPasscode', + ), ), ); @@ -197,8 +204,7 @@ void main() { verify(accountRemoteDataSource.login(email, encodedPasscode)).called(1); verifyNoMoreInteractions(accountRemoteDataSource); - verify(secureStorage.readEmail()).called(1); - verify(secureStorage.readEncodedPasscode()).called(1); + verify(secureStorage.getAuthenticatedUser()).called(1); verify(secureStorage.updateToken(newToken)).called(1); verifyNoMoreInteractions(secureStorage); @@ -223,19 +229,24 @@ void main() { int counter = 0; String getNewToken() => '${++counter}'; - when(secureStorage.readEmail()).thenAnswer( - (_) async => email, - ); - when(secureStorage.readEncodedPasscode()).thenAnswer( - (_) async => encodedPasscode, - ); - when(secureStorage.readToken()).thenAnswer( - (_) async => oldToken, + when(secureStorage.getAuthenticatedUser()).thenAnswer( + (_) async => some( + const AuthenticatedUserModel( + email: email, + token: oldToken, + encodedPasscode: encodedPasscode, + ), + ), ); when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( - (_) async => - right(AuthenticatedUser(email: email, token: getNewToken())), + (_) async => right( + AuthenticatedUser( + email: email, + token: getNewToken(), + encodedPasscode: 'encodedPasscode', + ), + ), ); final request = _requestFromMethod('GET'); @@ -270,16 +281,24 @@ void main() { const encodedPasscode = 'encodedPasscode'; const newToken = 'newToken'; - when(secureStorage.readEmail()).thenAnswer( - (_) async => email, - ); - when(secureStorage.readEncodedPasscode()).thenAnswer( - (_) async => encodedPasscode, + when(secureStorage.getAuthenticatedUser()).thenAnswer( + (_) async => some( + const AuthenticatedUserModel( + email: email, + token: newToken, + encodedPasscode: encodedPasscode, + ), + ), ); when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( - (_) async => - right(const AuthenticatedUser(email: email, token: newToken)), + (_) async => right( + const AuthenticatedUser( + email: email, + token: newToken, + encodedPasscode: 'encodedPasscode', + ), + ), ); final request = _requestFromMethod('GET'); @@ -311,15 +330,15 @@ chopper.Request _requestFromMethod(String method) { class _FakeGetIt extends Fake implements GetIt { _FakeGetIt.fromMockedObjects({ - required this.mockAuthenticationCubit, - required this.mockAccountRemoteDataSource, - required this.mockSecureStorage, + required this.authenticationCubit, + required this.accountRemoteDataSource, + required this.authenticationLocalDataSource, required this.mockLogger, }); - final MockAuthenticationCubit mockAuthenticationCubit; - final MockAccountRemoteDataSource mockAccountRemoteDataSource; - final MockSecureStorage mockSecureStorage; + final MockAuthenticationCubit authenticationCubit; + final MockAccountRemoteDataSource accountRemoteDataSource; + final MockAuthenticationLocalDataSource authenticationLocalDataSource; final MockLogger mockLogger; @override @@ -339,9 +358,9 @@ class _FakeGetIt extends Fake implements GetIt { // ignore: type_annotate_public_apis T get({String? instanceName, param1, param2, Type? type}) { return switch (T) { - const (AuthenticationCubit) => mockAuthenticationCubit, - const (AccountRemoteDataSource) => mockAccountRemoteDataSource, - const (SecureStorage) => mockSecureStorage, + const (AuthenticationCubit) => authenticationCubit, + const (AccountRemoteDataSource) => accountRemoteDataSource, + const (AuthenticationLocalDataSource) => authenticationLocalDataSource, const (Logger) => mockLogger, _ => throw UnimplementedError('Mock for $T not implemented.'), } as T; @@ -350,9 +369,10 @@ class _FakeGetIt extends Fake implements GetIt { /// Given a mocked type, get the mocked object for the given type. T getMock() { return switch (T) { - const (MockAuthenticationCubit) => mockAuthenticationCubit, - const (MockAccountRemoteDataSource) => mockAccountRemoteDataSource, - const (MockSecureStorage) => mockSecureStorage, + const (MockAuthenticationCubit) => authenticationCubit, + const (MockAccountRemoteDataSource) => accountRemoteDataSource, + const (MockAuthenticationLocalDataSource) => + authenticationLocalDataSource, const (MockLogger) => mockLogger, _ => throw UnimplementedError('Mock for $T not implemented.'), } as T; diff --git a/test/fixtures/authenticated_user/authenticated_user.json b/test/fixtures/authenticated_user/authenticated_user.json new file mode 100644 index 000000000..66e9f9975 --- /dev/null +++ b/test/fixtures/authenticated_user/authenticated_user.json @@ -0,0 +1,5 @@ +{ + "email": "email", + "token": "token", + "passcode": "passcode" +} \ No newline at end of file diff --git a/test/fixtures/fixture_reader.dart b/test/fixtures/fixture_reader.dart new file mode 100644 index 000000000..147beab30 --- /dev/null +++ b/test/fixtures/fixture_reader.dart @@ -0,0 +1,5 @@ +import 'dart:io'; + +// ignore +// ignore: avoid-top-level-members-in-tests +String fixture(String name) => File('test/fixtures/$name').readAsStringSync();