Files
Immich/mobile/test/data/store/person_store_test.dart
Adam GastineauandGitHub 91247f1492 chore(mobile): migrate OCR, Tag, and UserMetadata to Store (#31704)
* chore(mobile): migrate OCR, Tag, and UserMetadata to Store

* Add comments to Tag HTTP repository

* Add mounted check
2026-09-22 18:29:33 +00:00

141 lines
5.1 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/data/server/person.dart';
import 'package:immich_mobile/data/store.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:mocktail/mocktail.dart';
import '../../medium/repository_context.dart';
class _MockPersonApi extends Mock implements PersonApiRepository {}
void main() {
late MediumRepositoryContext ctx;
late _MockPersonApi api;
late ProviderContainer container;
setUp(() {
ctx = MediumRepositoryContext();
api = _MockPersonApi();
container = ProviderContainer(
overrides: [
driftProvider.overrideWithValue(ctx.db),
personApiRepositoryProvider.overrideWithValue(api),
// No stored preferences: the default minimum face count applies
Store.userMetadata.preferences().overrideWith((ref) => Stream.value(null)),
],
);
addTearDown(container.dispose);
addTearDown(ctx.dispose);
});
Future<Person?> byId(String personId) => container.read(Store.people.byId(personId).future);
test('forAsset serves the people on the asset', () async {
final user = await ctx.newUser();
final asset = await ctx.newRemoteAsset(ownerId: user.id);
final person = await ctx.newPerson(ownerId: user.id);
await ctx.newFace(assetId: asset.id, personId: person.id);
final people = await container.read(Store.people.forAsset(asset.id).future);
expect(people.map((p) => p.id), [person.id]);
});
test('all serves named people, hiding unnamed ones lacking the face count', () async {
final user = await ctx.newUser();
final asset = await ctx.newRemoteAsset(ownerId: user.id);
final named = await ctx.newPerson(ownerId: user.id, name: 'Alice');
final unnamed = await ctx.newPerson(ownerId: user.id, name: '');
await ctx.newFace(assetId: asset.id, personId: named.id);
await ctx.newFace(assetId: asset.id, personId: unnamed.id);
final people = await container.read(Store.people.all().future);
expect(people.map((p) => p.id), [named.id]);
});
test('forAsset re-emits when a person is renamed', () async {
final user = await ctx.newUser();
final asset = await ctx.newRemoteAsset(ownerId: user.id);
final person = await ctx.newPerson(ownerId: user.id, name: 'Old');
await ctx.newFace(assetId: asset.id, personId: person.id);
when(() => api.update(person.id, name: 'New')).thenAnswer((_) async => Person(id: person.id, name: 'New'));
final renamed = Completer<void>();
final provider = Store.people.forAsset(asset.id);
container.listen(provider, (_, next) {
if (next.valueOrNull?.single.name == 'New' && !renamed.isCompleted) {
renamed.complete();
}
});
await container.read(provider.future);
await container.read(Store.people).updateName(person.id, 'New');
await expectLater(renamed.future, completes);
});
test('forAsset does not push updates unnecessarily', () async {
final user = await ctx.newUser();
final asset = await ctx.newRemoteAsset(ownerId: user.id);
final person = await ctx.newPerson(ownerId: user.id, name: 'Old');
await ctx.newFace(assetId: asset.id, personId: person.id);
var emissions = 0;
final provider = Store.people.forAsset(asset.id);
container.listen(provider, (_, _) => emissions += 1);
await container.read(provider.future);
await ctx.newPerson(ownerId: user.id, name: 'Unrelated');
await pumpEventQueue();
// Only should have received the first, loaded event
expect(emissions, 1);
});
test('updateName pushes to the server, then saves locally', () async {
final user = await ctx.newUser();
final person = await ctx.newPerson(ownerId: user.id, name: 'Old');
when(() => api.update(person.id, name: 'New')).thenAnswer((_) async => Person(id: person.id, name: 'New'));
await container.read(Store.people).updateName(person.id, 'New');
verify(() => api.update(person.id, name: 'New')).called(1);
expect((await byId(person.id))?.name, 'New');
});
test('updateName leaves the DB untouched when the server rejects', () async {
final user = await ctx.newUser();
final person = await ctx.newPerson(ownerId: user.id, name: 'Old');
when(() => api.update(person.id, name: 'New')).thenThrow(Exception('rejected'));
await expectLater(container.read(Store.people).updateName(person.id, 'New'), throwsException);
expect((await byId(person.id))?.name, 'Old');
});
test('updateBirthday pushes to the server, then saves locally', () async {
final user = await ctx.newUser();
final person = await ctx.newPerson(ownerId: user.id);
final birthday = DateTime.utc(1990, 4, 2);
when(
() => api.update(person.id, birthday: birthday),
).thenAnswer((_) async => Person(id: person.id, name: person.name, birthDate: birthday));
await container.read(Store.people).updateBirthday(person.id, birthday);
verify(() => api.update(person.id, birthday: birthday)).called(1);
expect((await byId(person.id))?.birthDate, birthday);
});
}