0
Fork 0
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:
shenlong-tanwen 2025-02-16 23:10:08 +05:30
parent 68daca2728
commit b6f490af74
28 changed files with 425 additions and 131 deletions

View file

@ -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);
}

View file

@ -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,
});

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -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);
}

View file

@ -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;

View file

@ -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.");

View file

@ -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),
);
});

View file

@ -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)

View file

@ -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;

View file

@ -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,

View file

@ -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();
}

View file

@ -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");
}

View file

@ -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!);
}

View file

@ -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,

View file

@ -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();
}
}

View 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);
});
});
}

View 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);
});
});
}

View file

@ -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",

View file

@ -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: '',
);
}

View 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 {}

View file

@ -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();

View file

@ -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',
);

View file

@ -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, '');
});

View file

@ -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',

View file

@ -19,7 +19,7 @@ final activity = Activity(
id: '1',
createdAt: DateTime(100),
type: ActivityType.like,
user: UserStub.admin,
user: UserStub.adminOld,
);
void main() {

View file

@ -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);

View file

@ -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});