mirror of https://github.com/flutter/samples.git
Compass App: WIP Auth logic refactor (#2394)
This PR follows up with the conversation we had via email and cleans up the auth implementation. **New `AuthRepository`** The `AuthTokenRepository`, `AuthLoginComponent` and `AuthLogoutComponent` have been refactored into a single `AuthRepository`. This `AuthRepository` exposes the `isAuthenticated` boolean, which is used by `go_router` to handle redirects. Also, has two methods, `login(email, pass)` and `logout()`, similar to what we had in the components. The implementation uses the new `SharedPreferencesService`, which is a service to access the `shared_preferences` plugin to store the `String` token. **New `AuthApiClient`** The `ApiClient` has been split in two. The `/login` REST call is moved to the `AuthApiClient`, while the other calls that require an auth token remain in the `ApiClient`. **`ApiClient` credentials** The main open question is, what is the best way to pass the auth token to the api client? I've found several alternatives: 1. `ApiClient` has a `token` setter which the `AuthRepository` calls to set the token - Implemented in this PR. - Follows our architecture design. - `AuthRepository` should be the source of truth, but the `ApiClient.token` could be set externally by mistake, so the single source of truth principle gets blurry. 2. `ApiClient` calls to the `AuthRepository` to obtain the token. - This is what the code was doing before. - `AuthRepository` is the source of truth. - But the dependency "Service to Repository" breaks our architecture design principle. 3. `ApiClient` calls to the `SharedPreferencesService` to obtain the token ( - `SharedPreferencesService` is the source of truth, as the `ApiClient` doesn't need the `AuthRepository`. - We see a "Service calling a Service" which maybe breaks our architecture design principle? 4. The `ApiClient` doesn't hold the token at all, intead, each call requires us to pass a token. - Tried to implement it but becomes very complex. - Makes other repositories depend on the `AuthRepository` or the `SharedPreferencesService`. Personal take: I have implemented 1 in this PR because it is the one that follows best the architecture recommendations. However I see some pitfalls, e.g. the `AuthRepository` needs to fetch the token from the `SharedPreferencesService` at least once on app start and pass it to the `ApiClient`. While on the solution 2 that's never a problem, since the `AuthRepository` is being called from the `ApiClient` and so it is always the source of truth for authentication. I am looking forward for your thoughts @ericwindmill ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.mdpull/2444/head
parent
56bf31fa21
commit
3be6873210
@ -0,0 +1,18 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
|
||||
abstract class AuthRepository extends ChangeNotifier {
|
||||
/// Returns true when the user is logged in
|
||||
/// Returns [Future] because it will load a stored auth state the first time.
|
||||
Future<bool> get isAuthenticated;
|
||||
|
||||
/// Perform login
|
||||
Future<Result<void>> login({
|
||||
required String email,
|
||||
required String password,
|
||||
});
|
||||
|
||||
/// Perform logout
|
||||
Future<Result<void>> logout();
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import '../../../utils/result.dart';
|
||||
import 'auth_repository.dart';
|
||||
|
||||
class AuthRepositoryDev extends AuthRepository {
|
||||
/// User is always authenticated in dev scenarios
|
||||
@override
|
||||
Future<bool> get isAuthenticated => Future.value(true);
|
||||
|
||||
/// Login is always successful in dev scenarios
|
||||
@override
|
||||
Future<Result<void>> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
return Result.ok(null);
|
||||
}
|
||||
|
||||
/// Logout is always successful in dev scenarios
|
||||
@override
|
||||
Future<Result<void>> logout() async {
|
||||
return Result.ok(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import '../../services/api_client.dart';
|
||||
import '../../services/auth_api_client.dart';
|
||||
import '../../services/shared_preferences_service.dart';
|
||||
import 'auth_repository.dart';
|
||||
|
||||
class AuthRepositoryRemote extends AuthRepository {
|
||||
AuthRepositoryRemote({
|
||||
required ApiClient apiClient,
|
||||
required AuthApiClient authApiClient,
|
||||
required SharedPreferencesService sharedPreferencesService,
|
||||
}) : _apiClient = apiClient,
|
||||
_authApiClient = authApiClient,
|
||||
_sharedPreferencesService = sharedPreferencesService {
|
||||
_apiClient.authHeaderProvider = _authHeaderProvider;
|
||||
}
|
||||
|
||||
final AuthApiClient _authApiClient;
|
||||
final ApiClient _apiClient;
|
||||
final SharedPreferencesService _sharedPreferencesService;
|
||||
|
||||
bool? _isAuthenticated;
|
||||
String? _authToken;
|
||||
final _log = Logger('AuthRepositoryRemote');
|
||||
|
||||
/// Fetch token from shared preferences
|
||||
Future<void> _fetch() async {
|
||||
final result = await _sharedPreferencesService.fetchToken();
|
||||
switch (result) {
|
||||
case Ok<String?>():
|
||||
_authToken = result.value;
|
||||
_isAuthenticated = result.value != null;
|
||||
case Error<String?>():
|
||||
_log.severe(
|
||||
'Failed to fech Token from SharedPreferences',
|
||||
result.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get isAuthenticated async {
|
||||
// Status is cached
|
||||
if (_isAuthenticated != null) {
|
||||
return _isAuthenticated!;
|
||||
}
|
||||
// No status cached, fetch from storage
|
||||
await _fetch();
|
||||
return _isAuthenticated ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _authApiClient.login(
|
||||
LoginRequest(
|
||||
email: email,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<LoginResponse>():
|
||||
_log.info('User logged int');
|
||||
// Set auth status
|
||||
_isAuthenticated = true;
|
||||
_authToken = result.value.token;
|
||||
// Store in Shared preferences
|
||||
return await _sharedPreferencesService.saveToken(result.value.token);
|
||||
case Error<LoginResponse>():
|
||||
_log.warning('Error logging in: ${result.error}');
|
||||
return Result.error(result.error);
|
||||
}
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> logout() async {
|
||||
_log.info('User logged out');
|
||||
try {
|
||||
// Clear stored auth token
|
||||
final result = await _sharedPreferencesService.saveToken(null);
|
||||
if (result is Error<void>) {
|
||||
_log.severe('Failed to clear stored auth token');
|
||||
}
|
||||
|
||||
// Clear token in ApiClient
|
||||
_authToken = null;
|
||||
|
||||
// Clear authenticated status
|
||||
_isAuthenticated = false;
|
||||
return result;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
String? _authHeaderProvider() =>
|
||||
_authToken != null ? 'Bearer $_authToken' : null;
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
|
||||
/// Repository to save and get auth token.
|
||||
/// Notifies listeners when the token changes e.g. user logs out.
|
||||
abstract class AuthTokenRepository extends ChangeNotifier {
|
||||
/// Get the token.
|
||||
/// If the value is null, usually means that the user is logged out.
|
||||
Future<Result<String?>> getToken();
|
||||
|
||||
/// Store the token.
|
||||
/// Will notifiy listeners.
|
||||
Future<Result<void>> saveToken(String? token);
|
||||
|
||||
/// Returns true when the token exists, otherwise false.
|
||||
Future<bool> hasToken() async {
|
||||
final result = await getToken();
|
||||
return result is Ok<String?> && result.value != null;
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import '../../../utils/result.dart';
|
||||
import 'auth_token_repository.dart';
|
||||
|
||||
/// Development [AuthTokenRepository] that always returns a fake token
|
||||
class AuthTokenRepositoryDev extends AuthTokenRepository {
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
return Result.ok('token');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
// TODO: Configurable baseurl/host/port
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:compass_model/model.dart';
|
||||
|
||||
import '../../utils/result.dart';
|
||||
|
||||
class AuthApiClient {
|
||||
Future<Result<LoginResponse>> login(LoginRequest loginRequest) async {
|
||||
final client = HttpClient();
|
||||
try {
|
||||
final request = await client.post('localhost', 8080, '/login');
|
||||
request.write(jsonEncode(loginRequest));
|
||||
final response = await request.close();
|
||||
if (response.statusCode == 200) {
|
||||
final stringData = await response.transform(utf8.decoder).join();
|
||||
return Result.ok(LoginResponse.fromJson(jsonDecode(stringData)));
|
||||
} else {
|
||||
return Result.error(const HttpException("Login error"));
|
||||
}
|
||||
} on Exception catch (error) {
|
||||
return Result.error(error);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,40 +1,36 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import 'auth_token_repository.dart';
|
||||
import '../../utils/result.dart';
|
||||
|
||||
/// [AuthTokenRepository] that stores the token in Shared Preferences.
|
||||
/// Provided for demo purposes, consider using a secure store instead.
|
||||
class AuthTokenRepositorySharedPrefs extends AuthTokenRepository {
|
||||
class SharedPreferencesService {
|
||||
static const _tokenKey = 'TOKEN';
|
||||
String? cachedToken;
|
||||
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
if (cachedToken != null) return Result.ok(cachedToken);
|
||||
final _log = Logger('SharedPreferencesService');
|
||||
|
||||
Future<Result<String?>> fetchToken() async {
|
||||
try {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final token = sharedPreferences.getString(_tokenKey);
|
||||
return Result.ok(token);
|
||||
_log.finer('Got token from SharedPreferences');
|
||||
return Result.ok(sharedPreferences.getString(_tokenKey));
|
||||
} on Exception catch (e) {
|
||||
_log.warning('Failed to get token', e);
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
try {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
if (token == null) {
|
||||
_log.finer('Removed token');
|
||||
await sharedPreferences.remove(_tokenKey);
|
||||
} else {
|
||||
_log.finer('Replaced token');
|
||||
await sharedPreferences.setString(_tokenKey, token);
|
||||
}
|
||||
cachedToken = token;
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
} on Exception catch (e) {
|
||||
_log.warning('Failed to set token', e);
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import '../../../data/repositories/auth/auth_token_repository.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
|
||||
/// Performs user login.
|
||||
class AuthLoginComponent {
|
||||
AuthLoginComponent({
|
||||
required AuthTokenRepository authTokenRepository,
|
||||
required ApiClient apiClient,
|
||||
}) : _authTokenRepository = authTokenRepository,
|
||||
_apiClient = apiClient;
|
||||
|
||||
final AuthTokenRepository _authTokenRepository;
|
||||
final ApiClient _apiClient;
|
||||
final _log = Logger('AuthLoginComponent');
|
||||
|
||||
/// Login with username and password.
|
||||
/// Performs login with the server and stores the obtained auth token.
|
||||
Future<Result<void>> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final result = await _apiClient.login(
|
||||
LoginRequest(
|
||||
email: email,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<LoginResponse>():
|
||||
_log.info('User logged int');
|
||||
return await _authTokenRepository.saveToken(result.value.token);
|
||||
case Error<LoginResponse>():
|
||||
_log.warning('Error logging in: ${result.error}');
|
||||
return Result.error(result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import '../../../data/repositories/auth/auth_token_repository.dart';
|
||||
import '../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
|
||||
|
||||
/// Performs user logout.
|
||||
class AuthLogoutComponent {
|
||||
AuthLogoutComponent({
|
||||
required AuthTokenRepository authTokenRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _authTokenRepository = authTokenRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository;
|
||||
|
||||
final AuthTokenRepository _authTokenRepository;
|
||||
final ItineraryConfigRepository _itineraryConfigRepository;
|
||||
final _log = Logger('AuthLogoutComponent');
|
||||
|
||||
/// Perform user logout.
|
||||
///
|
||||
/// 1. Clears the stored ItineraryConfig.
|
||||
/// 2. Clears the stored auth token.
|
||||
///
|
||||
/// GoRouter will automatically redirect the user to /login
|
||||
Future<Result<void>> logout() async {
|
||||
_log.info('User logged out');
|
||||
|
||||
// Clear stored ItineraryConfig
|
||||
var result = await _itineraryConfigRepository
|
||||
.setItineraryConfig(const ItineraryConfig());
|
||||
if (result is Error<void>) {
|
||||
_log.severe('Failed to clear stored ItineraryConfig');
|
||||
return result;
|
||||
}
|
||||
|
||||
// Clear stored auth token
|
||||
result = await _authTokenRepository.saveToken(null);
|
||||
if (result is Error<void>) {
|
||||
_log.severe('Failed to clear stored auth token');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,31 @@
|
||||
import '../../../../domain/components/auth/auth_logout_component.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
|
||||
import '../../../../data/repositories/auth/auth_repository.dart';
|
||||
import '../../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
|
||||
import '../../../../utils/command.dart';
|
||||
import '../../../../utils/result.dart';
|
||||
|
||||
class LogoutViewModel {
|
||||
LogoutViewModel({
|
||||
required AuthLogoutComponent authLogoutComponent,
|
||||
}) : _authLogoutComponent = authLogoutComponent {
|
||||
required AuthRepository authRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _authLogoutComponent = authRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
logout = Command0(_logout);
|
||||
}
|
||||
final AuthLogoutComponent _authLogoutComponent;
|
||||
final AuthRepository _authLogoutComponent;
|
||||
final ItineraryConfigRepository _itineraryConfigRepository;
|
||||
late Command0 logout;
|
||||
|
||||
Future<Result> _logout() => _authLogoutComponent.logout();
|
||||
Future<Result> _logout() async {
|
||||
var result = await _authLogoutComponent.logout();
|
||||
switch (result) {
|
||||
case Ok<void>():
|
||||
// clear stored itinerary config
|
||||
return _itineraryConfigRepository
|
||||
.setItineraryConfig(const ItineraryConfig());
|
||||
case Error<void>():
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
import 'package:compass_app/data/repositories/auth/auth_repository_remote.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../testing/fakes/services/fake_api_client.dart';
|
||||
import '../../../../testing/fakes/services/fake_auth_api_client.dart';
|
||||
import '../../../../testing/fakes/services/fake_shared_preferences_service.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthRepositoryRemote tests', () {
|
||||
late FakeApiClient apiClient;
|
||||
late FakeAuthApiClient authApiClient;
|
||||
late FakeSharedPreferencesService sharedPreferencesService;
|
||||
late AuthRepositoryRemote repository;
|
||||
|
||||
setUp(() {
|
||||
apiClient = FakeApiClient();
|
||||
authApiClient = FakeAuthApiClient();
|
||||
sharedPreferencesService = FakeSharedPreferencesService();
|
||||
repository = AuthRepositoryRemote(
|
||||
apiClient: apiClient,
|
||||
authApiClient: authApiClient,
|
||||
sharedPreferencesService: sharedPreferencesService,
|
||||
);
|
||||
});
|
||||
|
||||
test('fetch on start, has token', () async {
|
||||
// Stored token in shared preferences
|
||||
sharedPreferencesService.token = 'TOKEN';
|
||||
|
||||
// Create an AuthRepository, should perform initial fetch
|
||||
final repository = AuthRepositoryRemote(
|
||||
apiClient: apiClient,
|
||||
authApiClient: authApiClient,
|
||||
sharedPreferencesService: sharedPreferencesService,
|
||||
);
|
||||
|
||||
final isAuthenticated = await repository.isAuthenticated;
|
||||
|
||||
// True because Token is SharedPreferences
|
||||
expect(isAuthenticated, isTrue);
|
||||
|
||||
// Check auth token
|
||||
await expectAuthHeader(apiClient, 'Bearer TOKEN');
|
||||
});
|
||||
|
||||
test('fetch on start, no token', () async {
|
||||
// Stored token in shared preferences
|
||||
sharedPreferencesService.token = null;
|
||||
|
||||
// Create an AuthRepository, should perform initial fetch
|
||||
final repository = AuthRepositoryRemote(
|
||||
apiClient: apiClient,
|
||||
authApiClient: authApiClient,
|
||||
sharedPreferencesService: sharedPreferencesService,
|
||||
);
|
||||
|
||||
final isAuthenticated = await repository.isAuthenticated;
|
||||
|
||||
// True because Token is SharedPreferences
|
||||
expect(isAuthenticated, isFalse);
|
||||
|
||||
// Check auth token
|
||||
await expectAuthHeader(apiClient, null);
|
||||
});
|
||||
|
||||
test('perform login', () async {
|
||||
final result = await repository.login(
|
||||
email: 'EMAIL',
|
||||
password: 'PASSWORD',
|
||||
);
|
||||
expect(result, isA<Ok>());
|
||||
expect(await repository.isAuthenticated, isTrue);
|
||||
expect(sharedPreferencesService.token, 'TOKEN');
|
||||
|
||||
// Check auth token
|
||||
await expectAuthHeader(apiClient, 'Bearer TOKEN');
|
||||
});
|
||||
|
||||
test('perform logout', () async {
|
||||
// logged in status
|
||||
sharedPreferencesService.token = 'TOKEN';
|
||||
|
||||
final result = await repository.logout();
|
||||
expect(result, isA<Ok>());
|
||||
expect(await repository.isAuthenticated, isFalse);
|
||||
expect(sharedPreferencesService.token, null);
|
||||
|
||||
// Check auth token
|
||||
await expectAuthHeader(apiClient, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> expectAuthHeader(FakeApiClient apiClient, String? header) async {
|
||||
final header = apiClient.authHeaderProvider?.call();
|
||||
expect(header, header);
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_login_component.dart';
|
||||
import 'package:compass_app/data/services/api_client.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../../testing/fakes/services/fake_api_client.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthLoginComponent test', () {
|
||||
late AuthLoginComponent authLoginComponent;
|
||||
late FakeAuthTokenRepository authTokenRepository;
|
||||
late ApiClient apiClient;
|
||||
|
||||
setUp(() {
|
||||
authTokenRepository = FakeAuthTokenRepository();
|
||||
apiClient = _ApiClient();
|
||||
authLoginComponent = AuthLoginComponent(
|
||||
authTokenRepository: authTokenRepository,
|
||||
apiClient: apiClient,
|
||||
);
|
||||
});
|
||||
|
||||
test('should perform login', () async {
|
||||
// Pass valid email and password
|
||||
final result = await authLoginComponent.login(
|
||||
email: 'EMAIL',
|
||||
password: 'PASSWORD',
|
||||
);
|
||||
// Got good response
|
||||
expect(result, isA<Ok<void>>());
|
||||
expect(authTokenRepository.token, 'TOKEN');
|
||||
});
|
||||
|
||||
test('should fail to login', () async {
|
||||
// Pass wrong email and password
|
||||
final result = await authLoginComponent.login(
|
||||
email: 'WRONG',
|
||||
password: 'WRONG',
|
||||
);
|
||||
// Got bad response
|
||||
expect(result, isA<Error<void>>());
|
||||
expect(result.asError.error.toString(), 'Exception: ERROR');
|
||||
expect(authTokenRepository.token, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _ApiClient extends FakeApiClient {
|
||||
@override
|
||||
Future<Result<LoginResponse>> login(LoginRequest loginRequest) async {
|
||||
if (loginRequest.email == 'EMAIL' && loginRequest.password == 'PASSWORD') {
|
||||
return Result.ok(const LoginResponse(token: 'TOKEN', userId: '1234'));
|
||||
} else {
|
||||
return Result.error(Exception('ERROR'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_logout_component.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_itinerary_config_repository.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthLogoutComponent test', () {
|
||||
late AuthLogoutComponent authLogoutComponent;
|
||||
late FakeAuthTokenRepository authTokenRepository;
|
||||
late FakeItineraryConfigRepository itineraryConfigRepository;
|
||||
|
||||
setUp(() {
|
||||
authTokenRepository = FakeAuthTokenRepository();
|
||||
itineraryConfigRepository = FakeItineraryConfigRepository(
|
||||
itineraryConfig: const ItineraryConfig(continent: 'CONTINENT'),
|
||||
);
|
||||
authLogoutComponent = AuthLogoutComponent(
|
||||
authTokenRepository: authTokenRepository,
|
||||
itineraryConfigRepository: itineraryConfigRepository,
|
||||
);
|
||||
});
|
||||
|
||||
test('should perform logout', () async {
|
||||
await authLogoutComponent.logout();
|
||||
// Token should be removed
|
||||
expect(authTokenRepository.token, null);
|
||||
// Itinerary config should be cleared
|
||||
expect(
|
||||
itineraryConfigRepository.itineraryConfig,
|
||||
const ItineraryConfig(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import 'package:compass_app/data/repositories/auth/auth_repository.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
|
||||
class FakeAuthRepository extends AuthRepository {
|
||||
String? token;
|
||||
|
||||
@override
|
||||
Future<bool> get isAuthenticated async => token != null;
|
||||
|
||||
@override
|
||||
Future<Result<void>> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
token = 'TOKEN';
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> logout() async {
|
||||
token = null;
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import 'package:compass_app/data/services/auth_api_client.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
import 'package:compass_model/src/model/auth/login_request/login_request.dart';
|
||||
import 'package:compass_model/src/model/auth/login_response/login_response.dart';
|
||||
|
||||
class FakeAuthApiClient implements AuthApiClient {
|
||||
@override
|
||||
Future<Result<LoginResponse>> login(LoginRequest loginRequest) async {
|
||||
if (loginRequest.email == 'EMAIL' && loginRequest.password == 'PASSWORD') {
|
||||
return Result.ok(const LoginResponse(token: 'TOKEN', userId: '123'));
|
||||
}
|
||||
return Result.error(Exception('ERROR!'));
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,17 @@
|
||||
import 'package:compass_app/data/repositories/auth/auth_token_repository.dart';
|
||||
import 'package:compass_app/data/services/shared_preferences_service.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
|
||||
class FakeAuthTokenRepository extends AuthTokenRepository {
|
||||
class FakeSharedPreferencesService implements SharedPreferencesService {
|
||||
String? token;
|
||||
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
Future<Result<String?>> fetchToken() async {
|
||||
return Result.ok(token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
this.token = token;
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue