mirror of
https://github.com/immich-app/immich.git
synced 2025-02-18 01:24:26 -05:00
refactor: auth service and added tests
This commit is contained in:
parent
68daca2728
commit
b6f490af74
28 changed files with 425 additions and 131 deletions
|
@ -3,5 +3,5 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
|||
abstract interface class IUserRepository {
|
||||
Future<User?> tryGet(String id);
|
||||
|
||||
Future<User?> update(User user);
|
||||
Future<User> update(User user);
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ class User {
|
|||
this.profileImagePath = '',
|
||||
this.memoryEnabled = true,
|
||||
this.inTimeline = false,
|
||||
required this.quotaSizeInBytes,
|
||||
required this.quotaUsageInBytes,
|
||||
this.quotaSizeInBytes = 0,
|
||||
this.quotaUsageInBytes = 0,
|
||||
this.isPartnerSharedBy = false,
|
||||
this.isPartnerSharedWith = false,
|
||||
});
|
||||
|
|
|
@ -13,49 +13,50 @@ class AuthService {
|
|||
User? _currentUser;
|
||||
late final StreamController<User?> _userStreamController;
|
||||
|
||||
String? _currentUserId;
|
||||
late final StreamSubscription<String?> _userIdStream;
|
||||
|
||||
AuthService({
|
||||
required IStoreRepository storeRepo,
|
||||
required IUserRepository userRepo,
|
||||
}) : _storeRepo = storeRepo,
|
||||
_userRepo = userRepo {
|
||||
_userStreamController = StreamController.broadcast();
|
||||
_userIdStream = _storeRepo.watch(StoreKey.currentUserId).listen((userId) {
|
||||
if (_currentUserId == userId) {
|
||||
return;
|
||||
}
|
||||
_currentUserId = userId;
|
||||
unawaited(loadOfflineUser());
|
||||
});
|
||||
// Pre-load offline user
|
||||
unawaited(_loadOfflineUser());
|
||||
}
|
||||
|
||||
void _updateCurrentUser(User? user) {
|
||||
void _notifyListeners(User? user) {
|
||||
_currentUser = user;
|
||||
_userStreamController.add(user);
|
||||
}
|
||||
|
||||
User getCurrentUser() {
|
||||
Future<void> _loadOfflineUser() async {
|
||||
final userId = await _storeRepo.tryGet(StoreKey.currentUserId);
|
||||
if (userId == null) {
|
||||
_currentUser = null;
|
||||
return;
|
||||
}
|
||||
final user = await _userRepo.tryGet(userId);
|
||||
_notifyListeners(user);
|
||||
}
|
||||
|
||||
User? tryGetUser() => _currentUser;
|
||||
|
||||
User getUser() {
|
||||
if (_currentUser == null) {
|
||||
throw const UserNotLoggedInException();
|
||||
}
|
||||
return _currentUser!;
|
||||
}
|
||||
|
||||
User? tryGetCurrentUser() => _currentUser;
|
||||
Stream<User?> watchUser() => _userStreamController.stream;
|
||||
|
||||
Stream<User?> watchCurrentUser() => _userStreamController.stream;
|
||||
|
||||
Future<User?> loadOfflineUser() async {
|
||||
if (_currentUserId == null) return null;
|
||||
final user = await _userRepo.tryGet(_currentUserId!);
|
||||
_updateCurrentUser(user);
|
||||
Future<User?> updateUser(User user) async {
|
||||
await _userRepo.update(user);
|
||||
await _storeRepo.update(StoreKey.currentUserId, user.id);
|
||||
_notifyListeners(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<void> cleanup() async {
|
||||
await _userIdStream.cancel();
|
||||
await _userStreamController.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,17 +24,21 @@ class StoreService {
|
|||
return _instance!;
|
||||
}
|
||||
|
||||
// TODO: Replace the implementation with the one from create after removing the typedef
|
||||
/// Initializes the store with the given [storeRepository]
|
||||
static Future<StoreService> init(IStoreRepository storeRepository) async {
|
||||
if (_instance != null) {
|
||||
return _instance!;
|
||||
}
|
||||
_instance = StoreService._(storeRepository);
|
||||
await _instance!._populateCache();
|
||||
_instance!._subscription = _instance!._listenForChange();
|
||||
_instance ??= await create(storeRepository);
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// Initializes the store with the given [storeRepository]
|
||||
static Future<StoreService> create(IStoreRepository storeRepository) async {
|
||||
final instance = StoreService._(storeRepository);
|
||||
await instance._populateCache();
|
||||
instance._subscription = instance._listenForChange();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// Fills the cache with the values from the DB
|
||||
Future<void> _populateCache() async {
|
||||
for (StoreKey key in StoreKey.values) {
|
||||
|
|
|
@ -6,5 +6,5 @@ class UserService {
|
|||
|
||||
const UserService({required IUserRepository userRepo}) : _userRepo = userRepo;
|
||||
|
||||
Future<User?> updateUser(User user) => _userRepo.update(user);
|
||||
Future<User> updateUser(User user) => _userRepo.update(user);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class IsarUserRepository extends IsarDatabaseRepository
|
|||
}
|
||||
|
||||
@override
|
||||
Future<User?> update(User user) {
|
||||
Future<User> update(User user) {
|
||||
return nestTxn(() async {
|
||||
await _db.users.put(user.toOldUser());
|
||||
return user;
|
||||
|
|
|
@ -4,13 +4,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/auth.service.dart' as nauth;
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/models/auth/auth_state.model.dart';
|
||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/domain/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/domain/user.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
|
@ -22,7 +20,6 @@ final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
|||
ref.watch(authServiceProviderOld),
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(authServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -30,17 +27,12 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||
final AuthService _authService;
|
||||
final ApiService _apiService;
|
||||
final nauth.AuthService _nAuthService;
|
||||
final UserService _userService;
|
||||
final _log = Logger("AuthenticationNotifier");
|
||||
|
||||
static const Duration _timeoutDuration = Duration(seconds: 7);
|
||||
|
||||
AuthNotifier(
|
||||
this._authService,
|
||||
this._apiService,
|
||||
this._nAuthService,
|
||||
this._userService,
|
||||
) : super(
|
||||
AuthNotifier(this._authService, this._apiService, this._nAuthService)
|
||||
: super(
|
||||
AuthState(
|
||||
deviceId: "",
|
||||
userId: "",
|
||||
|
@ -115,8 +107,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||
String deviceId =
|
||||
Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
|
||||
|
||||
_nAuthService.loadOfflineUser();
|
||||
User? user = _nAuthService.tryGetCurrentUser()?.toOldUser();
|
||||
User? user = _nAuthService.tryGetUser()?.toOldUser();
|
||||
|
||||
UserAdminResponseDto? userResponse;
|
||||
UserPreferencesResponseDto? userPreferences;
|
||||
|
@ -155,13 +146,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||
if (userResponse != null) {
|
||||
await Store.put(StoreKey.deviceId, deviceId);
|
||||
await Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
|
||||
user = (await _userService.updateUser(
|
||||
final updatedUser = await _nAuthService.updateUser(
|
||||
User.fromUserDto(userResponse, userPreferences).toDomain(),
|
||||
))
|
||||
?.toOldUser();
|
||||
if (user != null) {
|
||||
await Store.put(StoreKey.currentUserId, user.id);
|
||||
}
|
||||
);
|
||||
user = updatedUser?.toOldUser();
|
||||
await Store.put(StoreKey.accessToken, accessToken);
|
||||
} else {
|
||||
_log.severe("Unable to get user information from the server.");
|
||||
|
|
|
@ -1,31 +1,26 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/auth.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/domain/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/domain/user.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class CurrentUserProvider extends StateNotifier<User?> {
|
||||
CurrentUserProvider(this._apiService, this._authService, this._userService)
|
||||
: super(null) {
|
||||
state = _authService.tryGetCurrentUser()?.toOldUser();
|
||||
CurrentUserProvider(this._apiService, this._authService) : super(null) {
|
||||
state = _authService.tryGetUser()?.toOldUser();
|
||||
streamSub = _authService
|
||||
.watchCurrentUser()
|
||||
.watchUser()
|
||||
.map((user) => user?.toOldUser())
|
||||
.listen((user) => state = user);
|
||||
}
|
||||
|
||||
final ApiService _apiService;
|
||||
final AuthService _authService;
|
||||
final UserService _userService;
|
||||
|
||||
late final StreamSubscription<User?> streamSub;
|
||||
|
||||
// TODO: Move method this to AuthService
|
||||
|
@ -34,13 +29,8 @@ class CurrentUserProvider extends StateNotifier<User?> {
|
|||
final user = await _apiService.usersApi.getMyUser();
|
||||
if (user != null) {
|
||||
final userPreferences = await _apiService.usersApi.getMyPreferences();
|
||||
final updatedUser = (await _userService.updateUser(
|
||||
User.fromUserDto(user, userPreferences).toDomain(),
|
||||
))
|
||||
?.toOldUser();
|
||||
if (updatedUser != null) {
|
||||
await Store.put(StoreKey.currentUserId, updatedUser.id);
|
||||
}
|
||||
await _authService
|
||||
.updateUser(User.fromUserDto(user, userPreferences).toDomain());
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
@ -57,7 +47,6 @@ final currentUserProvider =
|
|||
return CurrentUserProvider(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(authServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
|||
if (shared != null) {
|
||||
query = query.sharedEqualTo(shared);
|
||||
}
|
||||
final user = _authService.getCurrentUser().toOldUser();
|
||||
final user = _authService.getUser().toOldUser();
|
||||
if (owner == true) {
|
||||
query = query.owner((q) => q.isarIdEqualTo(user.isarId));
|
||||
} else if (owner == false) {
|
||||
|
@ -140,7 +140,7 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
|||
String searchTerm,
|
||||
QuickFilterMode filterMode,
|
||||
) async {
|
||||
final user = _authService.getCurrentUser().toOldUser();
|
||||
final user = _authService.getUser().toOldUser();
|
||||
var query = db.albums
|
||||
.filter()
|
||||
.nameContains(searchTerm, caseSensitive: false)
|
||||
|
|
|
@ -98,7 +98,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
|||
shared: false,
|
||||
activityEnabled: false,
|
||||
);
|
||||
album.owner.value = _authService.getCurrentUser().toOldUser();
|
||||
album.owner.value = _authService.getUser().toOldUser();
|
||||
album.localId = assetPathEntity.id;
|
||||
album.isAll = assetPathEntity.isAll;
|
||||
return album;
|
||||
|
|
|
@ -33,7 +33,7 @@ class AssetMediaRepository implements IAssetMediaRepository {
|
|||
final Asset asset = Asset(
|
||||
checksum: "",
|
||||
localId: local.id,
|
||||
ownerId: authService.getCurrentUser().toOldUser().isarId,
|
||||
ownerId: authService.getUser().toOldUser().isarId,
|
||||
fileCreatedAt: local.createDateTime,
|
||||
fileModifiedAt: local.modifiedDateTime,
|
||||
updatedAt: local.modifiedDateTime,
|
||||
|
|
|
@ -29,7 +29,7 @@ class UserRepository extends DatabaseRepository implements IUserRepository {
|
|||
|
||||
@override
|
||||
Future<List<User>> getAll({bool self = true, UserSort? sortBy}) {
|
||||
final user = _authService.getCurrentUser().toOldUser();
|
||||
final user = _authService.getUser().toOldUser();
|
||||
final baseQuery = db.users.where();
|
||||
final int userId = user.isarId;
|
||||
final QueryBuilder<User, User, QAfterWhereClause> afterWhere =
|
||||
|
@ -48,7 +48,7 @@ class UserRepository extends DatabaseRepository implements IUserRepository {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<User> me() => Future.value(_authService.getCurrentUser().toOldUser());
|
||||
Future<User> me() => Future.value(_authService.getUser().toOldUser());
|
||||
|
||||
@override
|
||||
Future<void> deleteById(List<int> ids) => txn(() => db.users.deleteAll(ids));
|
||||
|
@ -64,6 +64,6 @@ class UserRepository extends DatabaseRepository implements IUserRepository {
|
|||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.or()
|
||||
.isarIdEqualTo(_authService.getCurrentUser().toOldUser().isarId)
|
||||
.isarIdEqualTo(_authService.getUser().toOldUser().isarId)
|
||||
.findAll();
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/domain/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/domain/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
|
||||
|
@ -14,9 +12,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||
/// Riverpod Instance
|
||||
final WidgetRef ref;
|
||||
|
||||
TabNavigationObserver({
|
||||
required this.ref,
|
||||
});
|
||||
TabNavigationObserver({required this.ref});
|
||||
|
||||
@override
|
||||
Future<void> didChangeTabRoute(
|
||||
|
@ -38,15 +34,11 @@ class TabNavigationObserver extends AutoRouterObserver {
|
|||
return;
|
||||
}
|
||||
|
||||
final user = (await ref.read(userServiceProvider).updateUser(
|
||||
User.fromUserDto(userResponseDto, userPreferences).toDomain(),
|
||||
))
|
||||
?.toOldUser();
|
||||
if (user != null) {
|
||||
await Store.put(StoreKey.currentUserId, user.id);
|
||||
}
|
||||
await ref.read(authServiceProvider).updateUser(
|
||||
User.fromUserDto(userResponseDto, userPreferences).toDomain(),
|
||||
);
|
||||
|
||||
ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||
await ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||
} catch (e) {
|
||||
debugPrint("Error refreshing user info $e");
|
||||
}
|
||||
|
|
|
@ -295,7 +295,7 @@ class AlbumService {
|
|||
|
||||
Future<bool> deleteAlbum(Album album) async {
|
||||
try {
|
||||
final userId = _authService.getCurrentUser().toOldUser().isarId;
|
||||
final userId = _authService.getUser().toOldUser().isarId;
|
||||
if (album.owner.value?.isarId == userId) {
|
||||
await _albumApiRepository.delete(album.remoteId!);
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ class BackupVerificationService {
|
|||
|
||||
/// Returns at most [limit] assets that were backed up without exif
|
||||
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
|
||||
final owner = _authService.getCurrentUser().toOldUser().isarId;
|
||||
final owner = _authService.getUser().toOldUser().isarId;
|
||||
final List<Asset> onlyLocal = await _assetRepository.getAll(
|
||||
ownerId: owner,
|
||||
state: AssetState.local,
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/domain/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/domain/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
|
@ -70,12 +68,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||
);
|
||||
if (user != null) {
|
||||
user.profileImagePath = profileImagePath;
|
||||
final updatedUser = (await ref
|
||||
.read(userServiceProvider)
|
||||
.updateUser(user.toDomain()));
|
||||
if (updatedUser != null) {
|
||||
await Store.put(StoreKey.currentUserId, updatedUser.id);
|
||||
}
|
||||
await ref.read(authServiceProvider).updateUser(user.toDomain());
|
||||
ref.read(currentUserProvider.notifier).refresh();
|
||||
}
|
||||
}
|
||||
|
|
124
mobile/test/domain/auth_service_test.dart
Normal file
124
mobile/test/domain/auth_service_test.dart
Normal file
|
@ -0,0 +1,124 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/exceptions/auth.exception.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/auth.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../fixtures/user.stub.dart';
|
||||
import '../infrastructure/repository.mock.dart';
|
||||
|
||||
void main() {
|
||||
late AuthService sut;
|
||||
late IUserRepository mockUserRepo;
|
||||
late IStoreRepository mockStoreRepo;
|
||||
|
||||
setUp(() {
|
||||
mockUserRepo = MockUserRepository();
|
||||
mockStoreRepo = MockStoreRepository();
|
||||
|
||||
registerFallbackValue(StoreKey.currentUserId);
|
||||
registerFallbackValue(UserStub.admin);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await sut.cleanup();
|
||||
});
|
||||
|
||||
group('AuthService Init:', () {
|
||||
test('Loads the offline user on init', () async {
|
||||
when(() => mockStoreRepo.tryGet<String>(any()))
|
||||
.thenAnswer((_) async => UserStub.admin.id);
|
||||
when(() => mockUserRepo.tryGet(any()))
|
||||
.thenAnswer((_) async => UserStub.admin);
|
||||
sut = AuthService(storeRepo: mockStoreRepo, userRepo: mockUserRepo);
|
||||
await pumpEventQueue();
|
||||
|
||||
verify(() => mockStoreRepo.tryGet<String>(StoreKey.currentUserId))
|
||||
.called(1);
|
||||
verify(() => mockUserRepo.tryGet(any())).called(1);
|
||||
expect(sut.tryGetUser(), UserStub.admin);
|
||||
});
|
||||
|
||||
test('Null current user when no user is stored', () {
|
||||
when(() => mockStoreRepo.tryGet<String>(any()))
|
||||
.thenAnswer((_) async => null);
|
||||
sut = AuthService(storeRepo: mockStoreRepo, userRepo: mockUserRepo);
|
||||
|
||||
expect(sut.tryGetUser(), isNull);
|
||||
verifyNever(() => mockUserRepo.tryGet(any()));
|
||||
});
|
||||
});
|
||||
|
||||
group('AuthService Get:', () {
|
||||
test('Throws UserNotLoggedInException when user is unavailable', () {
|
||||
when(() => mockStoreRepo.tryGet<String>(any()))
|
||||
.thenAnswer((_) async => null);
|
||||
sut = AuthService(storeRepo: mockStoreRepo, userRepo: mockUserRepo);
|
||||
expect(() => sut.getUser(), throwsA(isA<UserNotLoggedInException>()));
|
||||
});
|
||||
|
||||
test('Returns the current user', () async {
|
||||
when(() => mockStoreRepo.tryGet<String>(any()))
|
||||
.thenAnswer((_) async => UserStub.admin.id);
|
||||
when(() => mockUserRepo.tryGet(any()))
|
||||
.thenAnswer((_) async => UserStub.admin);
|
||||
|
||||
sut = AuthService(storeRepo: mockStoreRepo, userRepo: mockUserRepo);
|
||||
await pumpEventQueue();
|
||||
expect(sut.getUser(), UserStub.admin);
|
||||
});
|
||||
});
|
||||
|
||||
group('AuthService Try Get:', () {
|
||||
test('Returns null when user is unavailable', () {
|
||||
when(() => mockStoreRepo.tryGet<String>(any()))
|
||||
.thenAnswer((_) async => null);
|
||||
sut = AuthService(storeRepo: mockStoreRepo, userRepo: mockUserRepo);
|
||||
expect(sut.tryGetUser(), isNull);
|
||||
});
|
||||
|
||||
test('Returns the current user', () async {
|
||||
when(() => mockStoreRepo.tryGet<String>(any()))
|
||||
.thenAnswer((_) async => UserStub.admin.id);
|
||||
when(() => mockUserRepo.tryGet(any()))
|
||||
.thenAnswer((_) async => UserStub.admin);
|
||||
|
||||
sut = AuthService(storeRepo: mockStoreRepo, userRepo: mockUserRepo);
|
||||
await pumpEventQueue();
|
||||
expect(sut.tryGetUser(), UserStub.admin);
|
||||
});
|
||||
});
|
||||
|
||||
group('AuthService Update:', () {
|
||||
setUp(() {
|
||||
when(() => mockUserRepo.update(any()))
|
||||
.thenAnswer((_) async => UserStub.admin);
|
||||
when(() => mockStoreRepo.update<String>(any(), any()))
|
||||
.thenAnswer((_) async => true);
|
||||
});
|
||||
|
||||
test('Updates the user and internal cache', () async {
|
||||
when(() => mockStoreRepo.tryGet<String>(any()))
|
||||
.thenAnswer((_) async => null);
|
||||
sut = AuthService(storeRepo: mockStoreRepo, userRepo: mockUserRepo);
|
||||
|
||||
await sut.updateUser(UserStub.admin);
|
||||
expect(sut.tryGetUser(), UserStub.admin);
|
||||
verify(() => mockUserRepo.update(UserStub.admin)).called(1);
|
||||
verify(() =>
|
||||
mockStoreRepo.update(StoreKey.currentUserId, UserStub.admin.id))
|
||||
.called(1);
|
||||
});
|
||||
|
||||
test('Notifies listeners', () async {
|
||||
when(() => mockStoreRepo.tryGet<String>(any()))
|
||||
.thenAnswer((_) async => null);
|
||||
sut = AuthService(storeRepo: mockStoreRepo, userRepo: mockUserRepo);
|
||||
final stream = sut.watchUser();
|
||||
expectLater(stream, emits(UserStub.admin));
|
||||
await sut.updateUser(UserStub.admin);
|
||||
});
|
||||
});
|
||||
}
|
185
mobile/test/domain/store_service_test.dart
Normal file
185
mobile/test/domain/store_service_test.dart
Normal file
|
@ -0,0 +1,185 @@
|
|||
// ignore_for_file: avoid-dynamic
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/exceptions/store.exception.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../infrastructure/repository.mock.dart';
|
||||
|
||||
const _kAccessToken = '#ThisIsAToken';
|
||||
const _kBackgroundBackup = false;
|
||||
const _kGroupAssetsBy = 2;
|
||||
final _kBackupFailedSince = DateTime.utc(2023);
|
||||
|
||||
void main() {
|
||||
late StoreService sut;
|
||||
late IStoreRepository mockStoreRepo;
|
||||
late StreamController<StoreUpdateEvent> controller;
|
||||
|
||||
setUp(() async {
|
||||
controller = StreamController<StoreUpdateEvent>.broadcast();
|
||||
mockStoreRepo = MockStoreRepository();
|
||||
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
|
||||
registerFallbackValue(StoreKey.accessToken);
|
||||
registerFallbackValue(StoreKey.backupTriggerDelay);
|
||||
registerFallbackValue(StoreKey.backgroundBackup);
|
||||
registerFallbackValue(StoreKey.backupFailedSince);
|
||||
|
||||
when(() => mockStoreRepo.tryGet(any<StoreKey<dynamic>>()))
|
||||
.thenAnswer((invocation) async {
|
||||
final key = invocation.positionalArguments.firstOrNull as StoreKey;
|
||||
return switch (key) {
|
||||
StoreKey.accessToken => _kAccessToken,
|
||||
StoreKey.backgroundBackup => _kBackgroundBackup,
|
||||
StoreKey.groupAssetsBy => _kGroupAssetsBy,
|
||||
StoreKey.backupFailedSince => _kBackupFailedSince,
|
||||
// ignore: avoid-wildcard-cases-with-enums
|
||||
_ => null,
|
||||
};
|
||||
});
|
||||
when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
|
||||
|
||||
sut = await StoreService.create(mockStoreRepo);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
sut.dispose();
|
||||
await controller.close();
|
||||
});
|
||||
|
||||
group("Store Init:", () {
|
||||
test('Populates the internal cache on init', () {
|
||||
verify(() => mockStoreRepo.tryGet(any<StoreKey<dynamic>>()))
|
||||
.called(equals(StoreKey.values.length));
|
||||
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
|
||||
expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup);
|
||||
expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy);
|
||||
expect(sut.tryGet(StoreKey.backupFailedSince), _kBackupFailedSince);
|
||||
// Other keys should be null
|
||||
expect(sut.tryGet(StoreKey.currentUserId), isNull);
|
||||
});
|
||||
|
||||
test('Listens to stream of store updates', () async {
|
||||
final event =
|
||||
StoreUpdateEvent(StoreKey.accessToken, _kAccessToken.toUpperCase());
|
||||
controller.add(event);
|
||||
|
||||
await pumpEventQueue();
|
||||
|
||||
verify(() => mockStoreRepo.watchAll()).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase());
|
||||
});
|
||||
});
|
||||
|
||||
group('Store get:', () {
|
||||
test('Returns the stored value for the given key', () {
|
||||
expect(sut.get(StoreKey.accessToken), _kAccessToken);
|
||||
});
|
||||
|
||||
test('Throws StoreKeyNotFoundException for nonexistent keys', () {
|
||||
expect(
|
||||
() => sut.get(StoreKey.currentUserId),
|
||||
throwsA(isA<StoreKeyNotFoundException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('Returns the stored value for the given key or the defaultValue', () {
|
||||
expect(sut.get(StoreKey.currentUserId, 5), 5);
|
||||
});
|
||||
});
|
||||
|
||||
group('Store put:', () {
|
||||
setUp(() {
|
||||
when(() => mockStoreRepo.insert<String>(any<StoreKey<String>>(), any()))
|
||||
.thenAnswer((_) async => true);
|
||||
});
|
||||
|
||||
test('Skip insert when value is not modified', () async {
|
||||
await sut.put(StoreKey.accessToken, _kAccessToken);
|
||||
verifyNever(
|
||||
() => mockStoreRepo.insert<String>(StoreKey.accessToken, any()),
|
||||
);
|
||||
});
|
||||
|
||||
test('Insert value when modified', () async {
|
||||
final newAccessToken = _kAccessToken.toUpperCase();
|
||||
await sut.put(StoreKey.accessToken, newAccessToken);
|
||||
verify(
|
||||
() =>
|
||||
mockStoreRepo.insert<String>(StoreKey.accessToken, newAccessToken),
|
||||
).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
|
||||
});
|
||||
});
|
||||
|
||||
group('Store watch:', () {
|
||||
late StreamController<String?> valueController;
|
||||
|
||||
setUp(() {
|
||||
valueController = StreamController<String?>.broadcast();
|
||||
when(() => mockStoreRepo.watch<String>(any<StoreKey<String>>()))
|
||||
.thenAnswer((_) => valueController.stream);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await valueController.close();
|
||||
});
|
||||
|
||||
test('Watches a specific key for changes', () async {
|
||||
final stream = sut.watch(StoreKey.accessToken);
|
||||
final events = <String?>[
|
||||
_kAccessToken,
|
||||
_kAccessToken.toUpperCase(),
|
||||
null,
|
||||
_kAccessToken.toLowerCase(),
|
||||
];
|
||||
|
||||
expectLater(stream, emitsInOrder(events));
|
||||
|
||||
for (final event in events) {
|
||||
valueController.add(event);
|
||||
}
|
||||
|
||||
await pumpEventQueue();
|
||||
verify(() => mockStoreRepo.watch<String>(StoreKey.accessToken)).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('Store delete:', () {
|
||||
setUp(() {
|
||||
when(() => mockStoreRepo.delete<String>(any<StoreKey<String>>()))
|
||||
.thenAnswer((_) async => true);
|
||||
});
|
||||
|
||||
test('Removes the value from the DB', () async {
|
||||
await sut.delete(StoreKey.accessToken);
|
||||
verify(() => mockStoreRepo.delete<String>(StoreKey.accessToken))
|
||||
.called(1);
|
||||
});
|
||||
|
||||
test('Removes the value from the cache', () async {
|
||||
await sut.delete(StoreKey.accessToken);
|
||||
expect(sut.tryGet(StoreKey.accessToken), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('Store clear:', () {
|
||||
setUp(() {
|
||||
when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true);
|
||||
});
|
||||
|
||||
test('Clears all values from the store', () async {
|
||||
await sut.clear();
|
||||
verify(() => mockStoreRepo.deleteAll()).called(1);
|
||||
expect(sut.tryGet(StoreKey.accessToken), isNull);
|
||||
expect(sut.tryGet(StoreKey.backgroundBackup), isNull);
|
||||
expect(sut.tryGet(StoreKey.groupAssetsBy), isNull);
|
||||
expect(sut.tryGet(StoreKey.backupFailedSince), isNull);
|
||||
});
|
||||
});
|
||||
}
|
4
mobile/test/fixtures/album.stub.dart
vendored
4
mobile/test/fixtures/album.stub.dart
vendored
|
@ -26,7 +26,7 @@ final class AlbumStub {
|
|||
shared: true,
|
||||
activityEnabled: false,
|
||||
endDate: DateTime(2020),
|
||||
)..sharedUsers.addAll([UserStub.admin]);
|
||||
)..sharedUsers.addAll([UserStub.adminOld]);
|
||||
|
||||
static final oneAsset = Album(
|
||||
name: "album-with-single-asset",
|
||||
|
@ -53,7 +53,7 @@ final class AlbumStub {
|
|||
)
|
||||
..assets.addAll([AssetStub.image1, AssetStub.image2])
|
||||
..activityEnabled = true
|
||||
..owner.value = UserStub.admin;
|
||||
..owner.value = UserStub.adminOld;
|
||||
|
||||
static final create2020end2020Album = Album(
|
||||
name: "create2020update2020Album",
|
||||
|
|
27
mobile/test/fixtures/user.stub.dart
vendored
27
mobile/test/fixtures/user.stub.dart
vendored
|
@ -1,35 +1,46 @@
|
|||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart' as entity;
|
||||
|
||||
final class UserStub {
|
||||
abstract final class UserStub {
|
||||
const UserStub._();
|
||||
|
||||
static final admin = User(
|
||||
static final adminOld = entity.User(
|
||||
id: "admin",
|
||||
updatedAt: DateTime(2021),
|
||||
email: "admin@test.com",
|
||||
name: "admin",
|
||||
avatarColor: AvatarColorEnum.green,
|
||||
avatarColor: entity.AvatarColorEnum.green,
|
||||
profileImagePath: '',
|
||||
isAdmin: true,
|
||||
);
|
||||
|
||||
static final user1 = User(
|
||||
static final user1 = entity.User(
|
||||
id: "user1",
|
||||
updatedAt: DateTime(2022),
|
||||
email: "user1@test.com",
|
||||
name: "user1",
|
||||
avatarColor: AvatarColorEnum.red,
|
||||
avatarColor: entity.AvatarColorEnum.red,
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
);
|
||||
|
||||
static final user2 = User(
|
||||
static final user2 = entity.User(
|
||||
id: "user2",
|
||||
updatedAt: DateTime(2023),
|
||||
email: "user2@test.com",
|
||||
name: "user2",
|
||||
avatarColor: AvatarColorEnum.primary,
|
||||
avatarColor: entity.AvatarColorEnum.primary,
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
);
|
||||
|
||||
static final admin = User(
|
||||
id: "admin",
|
||||
name: "admin",
|
||||
email: "admin@immich.org",
|
||||
isAdmin: true,
|
||||
updatedAt: DateTime(2021),
|
||||
avatarColor: UserAvatarColor.green,
|
||||
profileImagePath: '',
|
||||
);
|
||||
}
|
||||
|
|
7
mobile/test/infrastructure/repository.mock.dart
Normal file
7
mobile/test/infrastructure/repository.mock.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockUserRepository extends Mock implements IUserRepository {}
|
||||
|
||||
class MockStoreRepository extends Mock implements IStoreRepository {}
|
|
@ -39,7 +39,7 @@ final _activities = [
|
|||
type: ActivityType.comment,
|
||||
comment: 'First Activity',
|
||||
assetId: 'asset-2',
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
),
|
||||
Activity(
|
||||
id: '2',
|
||||
|
@ -74,7 +74,7 @@ void main() {
|
|||
TestUtils.init();
|
||||
db = await TestUtils.initIsar();
|
||||
StoreService.init(IsarStoreRepository(db));
|
||||
await Store.put(StoreKey.currentUserId, UserStub.admin.id);
|
||||
await Store.put(StoreKey.currentUserId, UserStub.adminOld.id);
|
||||
await Store.put(StoreKey.serverEndpoint, '');
|
||||
await Store.put(StoreKey.accessToken, '');
|
||||
});
|
||||
|
@ -95,7 +95,7 @@ void main() {
|
|||
await db.writeTxn(() async {
|
||||
await db.clear();
|
||||
// Save all assets
|
||||
await db.users.put(UserStub.admin);
|
||||
await db.users.put(UserStub.adminOld);
|
||||
await db.assets.putAll([AssetStub.image1, AssetStub.image2]);
|
||||
await db.albums.put(AlbumStub.twoAsset);
|
||||
await AlbumStub.twoAsset.owner.save();
|
||||
|
|
|
@ -17,7 +17,7 @@ final _activities = [
|
|||
type: ActivityType.comment,
|
||||
comment: 'First Activity',
|
||||
assetId: 'asset-2',
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
),
|
||||
Activity(
|
||||
id: '2',
|
||||
|
@ -31,7 +31,7 @@ final _activities = [
|
|||
createdAt: DateTime(300),
|
||||
type: ActivityType.like,
|
||||
assetId: 'asset-1',
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
),
|
||||
Activity(
|
||||
id: '4',
|
||||
|
@ -108,7 +108,7 @@ void main() {
|
|||
id: '5',
|
||||
createdAt: DateTime(2023),
|
||||
type: ActivityType.like,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
);
|
||||
|
||||
when(
|
||||
|
@ -142,7 +142,7 @@ void main() {
|
|||
id: '5',
|
||||
createdAt: DateTime(2023),
|
||||
type: ActivityType.like,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
);
|
||||
when(
|
||||
() => activityMock.addActivity(
|
||||
|
@ -242,7 +242,7 @@ void main() {
|
|||
id: '5',
|
||||
createdAt: DateTime(2023),
|
||||
type: ActivityType.comment,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
comment: 'Test-Comment',
|
||||
assetId: 'test-asset',
|
||||
);
|
||||
|
@ -283,7 +283,7 @@ void main() {
|
|||
id: '5',
|
||||
createdAt: DateTime(2023),
|
||||
type: ActivityType.comment,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
assetId: 'test-asset',
|
||||
comment: 'Test-Comment',
|
||||
);
|
||||
|
@ -324,7 +324,7 @@ void main() {
|
|||
id: '5',
|
||||
createdAt: DateTime(2023),
|
||||
type: ActivityType.comment,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
comment: 'Test-Comment',
|
||||
assetId: 'test-asset',
|
||||
);
|
||||
|
|
|
@ -34,7 +34,7 @@ void main() {
|
|||
TestUtils.init();
|
||||
db = await TestUtils.initIsar();
|
||||
StoreService.init(IsarStoreRepository(db));
|
||||
Store.put(StoreKey.currentUserId, UserStub.admin.id);
|
||||
Store.put(StoreKey.currentUserId, UserStub.adminOld.id);
|
||||
Store.put(StoreKey.serverEndpoint, '');
|
||||
});
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ void main() {
|
|||
db = await TestUtils.initIsar();
|
||||
// For UserCircleAvatar
|
||||
StoreService.init(IsarStoreRepository(db));
|
||||
await Store.put(StoreKey.currentUserId, UserStub.admin.id);
|
||||
await Store.put(StoreKey.currentUserId, UserStub.adminOld.id);
|
||||
await Store.put(StoreKey.serverEndpoint, '');
|
||||
await Store.put(StoreKey.accessToken, '');
|
||||
});
|
||||
|
@ -47,7 +47,7 @@ void main() {
|
|||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.like,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
),
|
||||
),
|
||||
overrides: overrides,
|
||||
|
@ -64,7 +64,7 @@ void main() {
|
|||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.like,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
),
|
||||
),
|
||||
overrides: overrides,
|
||||
|
@ -83,7 +83,7 @@ void main() {
|
|||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.like,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
assetId: '1',
|
||||
),
|
||||
),
|
||||
|
@ -102,7 +102,7 @@ void main() {
|
|||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.like,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
assetId: '1',
|
||||
),
|
||||
),
|
||||
|
@ -121,7 +121,7 @@ void main() {
|
|||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.like,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
);
|
||||
|
||||
testWidgets('Like contains filled heart as leading', (tester) async {
|
||||
|
@ -172,7 +172,7 @@ void main() {
|
|||
createdAt: DateTime(100),
|
||||
type: ActivityType.comment,
|
||||
comment: 'This is a test comment',
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
);
|
||||
|
||||
testWidgets('Comment contains User Circle Avatar as leading',
|
||||
|
|
|
@ -19,7 +19,7 @@ final activity = Activity(
|
|||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.like,
|
||||
user: UserStub.admin,
|
||||
user: UserStub.adminOld,
|
||||
);
|
||||
|
||||
void main() {
|
||||
|
|
|
@ -36,7 +36,7 @@ void main() {
|
|||
TestUtils.init();
|
||||
db = await TestUtils.initIsar();
|
||||
await StoreService.init(IsarStoreRepository(db));
|
||||
await Store.put(StoreKey.currentUserId, UserStub.admin.id);
|
||||
await Store.put(StoreKey.currentUserId, UserStub.adminOld.id);
|
||||
mockApiService = MockApiService();
|
||||
mockSearchApi = MockSearchApi();
|
||||
when(() => mockApiService.searchApi).thenReturn(mockSearchApi);
|
||||
|
|
|
@ -34,10 +34,10 @@ void main() {
|
|||
..remoteThumbnailAssetId = AssetStub.image1.remoteId
|
||||
..assets.addAll([AssetStub.image1, AssetStub.image1])
|
||||
..owner.value = UserStub.user1
|
||||
..sharedUsers.addAll([UserStub.admin, UserStub.admin]);
|
||||
..sharedUsers.addAll([UserStub.adminOld, UserStub.adminOld]);
|
||||
|
||||
when(() => userRepository.get(album.ownerId!))
|
||||
.thenAnswer((_) async => UserStub.admin);
|
||||
.thenAnswer((_) async => UserStub.adminOld);
|
||||
|
||||
when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!))
|
||||
.thenAnswer((_) async => AssetStub.image1);
|
||||
|
@ -49,7 +49,7 @@ void main() {
|
|||
.thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]);
|
||||
|
||||
await sut.fillAlbumWithDatabaseEntities(album);
|
||||
expect(album.owner.value, UserStub.admin);
|
||||
expect(album.owner.value, UserStub.adminOld);
|
||||
expect(album.thumbnail.value, AssetStub.image1);
|
||||
expect(album.remoteUsers.toSet(), {UserStub.user1, UserStub.user2});
|
||||
expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2});
|
||||
|
|
Loading…
Add table
Reference in a new issue