From 084b4a8b14d93af79a513f0b594e18365fa0dde3 Mon Sep 17 00:00:00 2001 From: Chorn Date: Mon, 12 Dec 2022 22:28:29 +0700 Subject: [PATCH] [#15] Implement backend for logout functionality --- lib/api/repository/auth_repository.dart | 20 +++++++ lib/api/request/logout_request.dart | 21 ++++++++ lib/api/service/auth_service.dart | 6 +++ lib/database/shared_preferences_utils.dart | 7 +++ lib/usecase/logout_use_case.dart | 37 +++++++++++++ test/api/repository/auth_repository_test.dart | 15 ++++++ test/usecase/logout_use_case_test.dart | 53 +++++++++++++++++++ 7 files changed, 159 insertions(+) create mode 100644 lib/api/request/logout_request.dart create mode 100644 lib/usecase/logout_use_case.dart create mode 100644 test/usecase/logout_use_case_test.dart diff --git a/lib/api/repository/auth_repository.dart b/lib/api/repository/auth_repository.dart index 867b939..2d01959 100644 --- a/lib/api/repository/auth_repository.dart +++ b/lib/api/repository/auth_repository.dart @@ -2,6 +2,7 @@ import 'package:injectable/injectable.dart'; import 'package:survey/api/exception/network_exceptions.dart'; import 'package:survey/api/grant_type.dart'; import 'package:survey/api/request/login_request.dart'; +import 'package:survey/api/request/logout_request.dart'; import 'package:survey/api/request/reset_password_request.dart'; import 'package:survey/api/service/auth_service.dart'; import 'package:survey/env_variables.dart'; @@ -13,6 +14,10 @@ abstract class AuthRepository { required String password, }); + Future logout({ + required String token, + }); + Future resetPassword({ required String email, }); @@ -45,6 +50,21 @@ class AuthRepositoryImpl extends AuthRepository { } } + @override + Future logout({required String token}) async { + try { + await _authService.logout( + LogoutRequest( + token: token, + clientId: EnvVariables.clientId, + clientSecret: EnvVariables.clientSecret, + ), + ); + } catch (exception) { + throw NetworkExceptions.fromDioException(exception); + } + } + @override Future resetPassword({required String email}) async { try { diff --git a/lib/api/request/logout_request.dart b/lib/api/request/logout_request.dart new file mode 100644 index 0000000..208187c --- /dev/null +++ b/lib/api/request/logout_request.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'logout_request.g.dart'; + +@JsonSerializable() +class LogoutRequest { + final String token; + final String clientId; + final String clientSecret; + + LogoutRequest({ + required this.token, + required this.clientId, + required this.clientSecret, + }); + + factory LogoutRequest.fromJson(Map json) => + _$LogoutRequestFromJson(json); + + Map toJson() => _$LogoutRequestToJson(this); +} diff --git a/lib/api/service/auth_service.dart b/lib/api/service/auth_service.dart index 9a59b37..ad1b67e 100644 --- a/lib/api/service/auth_service.dart +++ b/lib/api/service/auth_service.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:retrofit/retrofit.dart'; import 'package:survey/api/request/login_request.dart'; +import 'package:survey/api/request/logout_request.dart'; import 'package:survey/api/request/reset_password_request.dart'; import 'package:survey/api/response/login_response.dart'; @@ -15,6 +16,11 @@ abstract class AuthService { @Body() LoginRequest body, ); + @POST('/api/v1/oauth/revoke') + Future logout( + @Body() LogoutRequest body, + ); + @POST('/api/v1/passwords') Future resetPassword( @Body() ResetPasswordRequest body, diff --git a/lib/database/shared_preferences_utils.dart b/lib/database/shared_preferences_utils.dart index 9ba2ff9..4507401 100644 --- a/lib/database/shared_preferences_utils.dart +++ b/lib/database/shared_preferences_utils.dart @@ -19,6 +19,8 @@ abstract class SharedPreferencesUtils { void saveTokenType(String tokenType); void saveRefreshToken(String refreshToken); + + void clear(); } @Singleton(as: SharedPreferencesUtils) @@ -54,4 +56,9 @@ class SharedPreferencesUtilsImpl extends SharedPreferencesUtils { void saveRefreshToken(String refreshToken) async { await _sharedPreferences.setString(_refreshTokenKey, refreshToken); } + + @override + void clear() async { + await _sharedPreferences.clear(); + } } diff --git a/lib/usecase/logout_use_case.dart b/lib/usecase/logout_use_case.dart new file mode 100644 index 0000000..1e584f9 --- /dev/null +++ b/lib/usecase/logout_use_case.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:injectable/injectable.dart'; +import 'package:survey/api/exception/network_exceptions.dart'; +import 'package:survey/api/repository/auth_repository.dart'; +import 'package:survey/database/hive_utils.dart'; +import 'package:survey/database/shared_preferences_utils.dart'; +import 'package:survey/usecase/base/base_use_case.dart'; + +@Injectable() +class LogoutUseCase extends NoInputUseCase { + final AuthRepository _repository; + final SharedPreferencesUtils _sharedPreferencesUtils; + final HiveUtils _hiveUtils; + + const LogoutUseCase( + this._repository, + this._sharedPreferencesUtils, + this._hiveUtils, + ); + + @override + Future> call() async { + final token = await _sharedPreferencesUtils.accessToken; + return _repository + .logout(token: token) + .then((value) => _clearTokensAndCache()) + .onError( + (exception, stackTrace) => Failed(UseCaseException(exception))); + } + + Result _clearTokensAndCache() { + _sharedPreferencesUtils.clear(); + _hiveUtils.clearSurveys(); + return Success(null); + } +} diff --git a/test/api/repository/auth_repository_test.dart b/test/api/repository/auth_repository_test.dart index 2be7b01..8b4db19 100644 --- a/test/api/repository/auth_repository_test.dart +++ b/test/api/repository/auth_repository_test.dart @@ -57,6 +57,21 @@ void main() { expect(result, throwsA(isA())); }); + test('When calling logout successfully, it returns empty result', () async { + when(mockAuthService.logout(any)).thenAnswer((_) async => null); + + await repository.logout(token: 'token'); + }); + + test('When calling logout failed, it returns NetworkExceptions error', + () async { + when(mockAuthService.logout(any)).thenThrow(MockDioError()); + + result() => repository.logout(token: 'token'); + + expect(result, throwsA(isA())); + }); + test('When calling reset password successfully, it returns empty result', () async { when(mockAuthService.resetPassword(any)).thenAnswer((_) async => null); diff --git a/test/usecase/logout_use_case_test.dart b/test/usecase/logout_use_case_test.dart new file mode 100644 index 0000000..5a7a6c8 --- /dev/null +++ b/test/usecase/logout_use_case_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:survey/api/exception/network_exceptions.dart'; +import 'package:survey/usecase/base/base_use_case.dart'; +import 'package:survey/usecase/logout_use_case.dart'; + +import '../../test/mock/mock_dependencies.mocks.dart'; + +void main() { + group('LogoutUseCaseTest', () { + late MockAuthRepository mockRepository; + late MockSharedPreferencesUtils mockSharedPreferencesUtils; + late MockHiveUtils mockHiveUtils; + late LogoutUseCase useCase; + + setUp(() { + mockRepository = MockAuthRepository(); + mockSharedPreferencesUtils = MockSharedPreferencesUtils(); + mockHiveUtils = MockHiveUtils(); + useCase = LogoutUseCase( + mockRepository, + mockSharedPreferencesUtils, + mockHiveUtils, + ); + }); + + test( + 'When executing use case and repository returns success, it returns Success result', + () async { + when(mockRepository.logout(token: anyNamed('token'))) + .thenAnswer((_) async => null); + + final result = await useCase.call(); + + expect(result, isA()); + verify(mockSharedPreferencesUtils.clear()).called(1); + verify(mockHiveUtils.clearSurveys()).called(1); + }); + + test( + 'When executing use case and repository returns error, it returns Failed result', + () async { + final exception = NetworkExceptions.badRequest(); + when(mockRepository.logout(token: anyNamed('token'))) + .thenAnswer((_) => Future.error(exception)); + + final result = await useCase.call(); + + expect(result, isA()); + expect((result as Failed).exception.actualException, exception); + }); + }); +}