// ignore_for_file: avoid-dynamic import 'dart:async'; import 'package:flutter_test/flutter_test.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 controller; setUp(() async { controller = StreamController.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>())) .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(storeRepository: mockStoreRepo); }); tearDown(() async { sut.dispose(); await controller.close(); }); group("Store Service Init:", () { test('Populates the internal cache on init', () { verify(() => mockStoreRepo.tryGet(any>())) .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.currentUser), 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 Service 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.currentUser), throwsA(isA()), ); }); test('Returns the stored value for the given key or the defaultValue', () { expect(sut.get(StoreKey.currentUser, 5), 5); }); }); group('Store Service put:', () { setUp(() { when(() => mockStoreRepo.insert(any>(), any())) .thenAnswer((_) async => true); }); test('Skip insert when value is not modified', () async { await sut.put(StoreKey.accessToken, _kAccessToken); verifyNever( () => mockStoreRepo.insert(StoreKey.accessToken, any()), ); }); test('Insert value when modified', () async { final newAccessToken = _kAccessToken.toUpperCase(); await sut.put(StoreKey.accessToken, newAccessToken); verify( () => mockStoreRepo.insert(StoreKey.accessToken, newAccessToken), ).called(1); expect(sut.tryGet(StoreKey.accessToken), newAccessToken); }); }); group('Store Service watch:', () { late StreamController valueController; setUp(() { valueController = StreamController.broadcast(); when(() => mockStoreRepo.watch(any>())) .thenAnswer((_) => valueController.stream); }); tearDown(() async { await valueController.close(); }); test('Watches a specific key for changes', () async { final stream = sut.watch(StoreKey.accessToken); final events = [ _kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase(), ]; expectLater(stream, emitsInOrder(events)); for (final event in events) { valueController.add(event); } await pumpEventQueue(); verify(() => mockStoreRepo.watch(StoreKey.accessToken)).called(1); }); }); group('Store Service delete:', () { setUp(() { when(() => mockStoreRepo.delete(any>())) .thenAnswer((_) async => true); }); test('Removes the value from the DB', () async { await sut.delete(StoreKey.accessToken); verify(() => mockStoreRepo.delete(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 Service 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); }); }); }