2025-04-17 20:55:27 +05:30
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
|
|
|
|
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
|
2025-08-06 11:54:18 +03:00
|
|
|
import 'package:immich_mobile/domain/services/trash.service.dart';
|
2025-06-23 11:27:44 -05:00
|
|
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
2025-06-19 18:25:18 -05:00
|
|
|
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
2025-04-17 20:55:27 +05:30
|
|
|
import 'package:mocktail/mocktail.dart';
|
|
|
|
|
|
|
|
|
|
import '../../fixtures/sync_stream.stub.dart';
|
|
|
|
|
import '../../infrastructure/repository.mock.dart';
|
2025-07-31 18:34:36 +03:00
|
|
|
import '../service.mock.dart';
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
class _AbortCallbackWrapper {
|
|
|
|
|
const _AbortCallbackWrapper();
|
|
|
|
|
|
|
|
|
|
bool call() => false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _MockAbortCallbackWrapper extends Mock implements _AbortCallbackWrapper {}
|
|
|
|
|
|
2025-04-17 20:55:27 +05:30
|
|
|
class _CancellationWrapper {
|
|
|
|
|
const _CancellationWrapper();
|
|
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
bool call() => false;
|
2025-04-17 20:55:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _MockCancellationWrapper extends Mock implements _CancellationWrapper {}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late SyncStreamService sut;
|
2025-06-19 18:25:18 -05:00
|
|
|
late SyncStreamRepository mockSyncStreamRepo;
|
2025-06-23 11:27:44 -05:00
|
|
|
late SyncApiRepository mockSyncApiRepo;
|
2025-08-06 11:54:18 +03:00
|
|
|
late TrashService mockTrashService;
|
2025-04-18 14:01:16 -05:00
|
|
|
late Function(List<SyncEvent>, Function()) handleEventsCallback;
|
|
|
|
|
late _MockAbortCallbackWrapper mockAbortCallbackWrapper;
|
2025-04-17 20:55:27 +05:30
|
|
|
|
|
|
|
|
successHandler(Invocation _) async => true;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
mockSyncStreamRepo = MockSyncStreamRepository();
|
|
|
|
|
mockSyncApiRepo = MockSyncApiRepository();
|
2025-04-18 14:01:16 -05:00
|
|
|
mockAbortCallbackWrapper = _MockAbortCallbackWrapper();
|
2025-08-06 11:54:18 +03:00
|
|
|
mockTrashService = MockTrashService();
|
2025-04-18 14:01:16 -05:00
|
|
|
when(() => mockAbortCallbackWrapper()).thenReturn(false);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-25 08:07:22 +05:30
|
|
|
when(() => mockSyncApiRepo.streamChanges(any())).thenAnswer((invocation) async {
|
2025-04-18 14:01:16 -05:00
|
|
|
handleEventsCallback = invocation.positionalArguments.first;
|
|
|
|
|
});
|
2025-04-17 20:55:27 +05:30
|
|
|
|
|
|
|
|
when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {});
|
|
|
|
|
|
2025-07-25 08:07:22 +05:30
|
|
|
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.updatePartnerV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.deletePartnerV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.updateAssetsV1(any())).thenAnswer(successHandler);
|
2025-06-26 19:20:39 +05:30
|
|
|
when(
|
2025-07-29 00:34:03 +05:30
|
|
|
() => mockSyncStreamRepo.updateAssetsV1(any(), debugLabel: any(named: 'debugLabel')),
|
2025-06-26 19:20:39 +05:30
|
|
|
).thenAnswer(successHandler);
|
2025-07-25 08:07:22 +05:30
|
|
|
when(() => mockSyncStreamRepo.deleteAssetsV1(any())).thenAnswer(successHandler);
|
2025-06-26 19:20:39 +05:30
|
|
|
when(
|
2025-07-29 00:34:03 +05:30
|
|
|
() => mockSyncStreamRepo.deleteAssetsV1(any(), debugLabel: any(named: 'debugLabel')),
|
2025-06-26 19:20:39 +05:30
|
|
|
).thenAnswer(successHandler);
|
2025-07-25 08:07:22 +05:30
|
|
|
when(() => mockSyncStreamRepo.updateAssetsExifV1(any())).thenAnswer(successHandler);
|
2025-06-26 19:20:39 +05:30
|
|
|
when(
|
2025-07-29 00:34:03 +05:30
|
|
|
() => mockSyncStreamRepo.updateAssetsExifV1(any(), debugLabel: any(named: 'debugLabel')),
|
2025-06-26 19:20:39 +05:30
|
|
|
).thenAnswer(successHandler);
|
2025-07-25 08:07:22 +05:30
|
|
|
when(() => mockSyncStreamRepo.updateMemoriesV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.deleteMemoriesV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any())).thenAnswer(successHandler);
|
2025-07-07 11:01:09 +08:00
|
|
|
when(
|
2025-07-29 00:34:03 +05:30
|
|
|
() => mockSyncStreamRepo.updateStacksV1(any(), debugLabel: any(named: 'debugLabel')),
|
2025-07-07 11:01:09 +08:00
|
|
|
).thenAnswer(successHandler);
|
|
|
|
|
when(
|
2025-07-29 00:34:03 +05:30
|
|
|
() => mockSyncStreamRepo.deleteStacksV1(any(), debugLabel: any(named: 'debugLabel')),
|
2025-07-07 11:01:09 +08:00
|
|
|
).thenAnswer(successHandler);
|
2025-07-25 08:07:22 +05:30
|
|
|
when(() => mockSyncStreamRepo.updateUserMetadatasV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.deleteUserMetadatasV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.updatePeopleV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.deletePeopleV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.updateAssetFacesV1(any())).thenAnswer(successHandler);
|
|
|
|
|
when(() => mockSyncStreamRepo.deleteAssetFacesV1(any())).thenAnswer(successHandler);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-31 21:26:02 +03:00
|
|
|
sut = SyncStreamService(
|
|
|
|
|
syncApiRepository: mockSyncApiRepo,
|
|
|
|
|
syncStreamRepository: mockSyncStreamRepo,
|
2025-08-06 11:54:18 +03:00
|
|
|
trashService: mockTrashService,
|
2025-07-31 21:26:02 +03:00
|
|
|
);
|
2025-04-17 20:55:27 +05:30
|
|
|
});
|
|
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
Future<void> simulateEvents(List<SyncEvent> events) async {
|
|
|
|
|
await sut.sync();
|
|
|
|
|
await handleEventsCallback(events, mockAbortCallbackWrapper.call);
|
2025-04-17 20:55:27 +05:30
|
|
|
}
|
|
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
group("SyncStreamService - _handleEvents", () {
|
2025-07-29 00:34:03 +05:30
|
|
|
test("processes events and acks successfully when handlers succeed", () async {
|
2025-04-17 20:55:27 +05:30
|
|
|
final events = [
|
2025-04-18 14:01:16 -05:00
|
|
|
SyncStreamStub.userDeleteV1,
|
|
|
|
|
SyncStreamStub.userV1Admin,
|
2025-07-29 00:34:03 +05:30
|
|
|
SyncStreamStub.userV1User,
|
|
|
|
|
SyncStreamStub.partnerDeleteV1,
|
|
|
|
|
SyncStreamStub.partnerV1,
|
2025-04-17 20:55:27 +05:30
|
|
|
];
|
|
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
await simulateEvents(events);
|
|
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
verifyInOrder([
|
|
|
|
|
() => mockSyncStreamRepo.deleteUsersV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["2"]),
|
|
|
|
|
() => mockSyncStreamRepo.updateUsersV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["5"]),
|
|
|
|
|
() => mockSyncStreamRepo.deletePartnerV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["4"]),
|
|
|
|
|
() => mockSyncStreamRepo.updatePartnerV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["3"]),
|
|
|
|
|
]);
|
|
|
|
|
verifyNever(() => mockAbortCallbackWrapper());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("processes final batch correctly", () async {
|
|
|
|
|
final events = [SyncStreamStub.userDeleteV1, SyncStreamStub.userV1Admin];
|
|
|
|
|
|
|
|
|
|
await simulateEvents(events);
|
|
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
verifyInOrder([
|
|
|
|
|
() => mockSyncStreamRepo.deleteUsersV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["2"]),
|
|
|
|
|
() => mockSyncStreamRepo.updateUsersV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["1"]),
|
|
|
|
|
]);
|
|
|
|
|
verifyNever(() => mockAbortCallbackWrapper());
|
2025-04-17 20:55:27 +05:30
|
|
|
});
|
|
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
test("does not process or ack when event list is empty", () async {
|
|
|
|
|
await simulateEvents([]);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
|
|
|
|
verifyNever(() => mockSyncStreamRepo.updateUsersV1(any()));
|
|
|
|
|
verifyNever(() => mockSyncStreamRepo.deleteUsersV1(any()));
|
|
|
|
|
verifyNever(() => mockSyncStreamRepo.updatePartnerV1(any()));
|
|
|
|
|
verifyNever(() => mockSyncStreamRepo.deletePartnerV1(any()));
|
2025-04-18 14:01:16 -05:00
|
|
|
verifyNever(() => mockAbortCallbackWrapper());
|
2025-04-17 20:55:27 +05:30
|
|
|
verifyNever(() => mockSyncApiRepo.ack(any()));
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
test("aborts and stops processing if cancelled during iteration", () async {
|
|
|
|
|
final cancellationChecker = _MockCancellationWrapper();
|
|
|
|
|
when(() => cancellationChecker()).thenReturn(false);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
sut = SyncStreamService(
|
|
|
|
|
syncApiRepository: mockSyncApiRepo,
|
|
|
|
|
syncStreamRepository: mockSyncStreamRepo,
|
2025-08-06 11:54:18 +03:00
|
|
|
trashService: mockTrashService,
|
2025-04-18 14:01:16 -05:00
|
|
|
cancelChecker: cancellationChecker.call,
|
|
|
|
|
);
|
|
|
|
|
await sut.sync();
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
final events = [SyncStreamStub.userDeleteV1, SyncStreamStub.userV1Admin, SyncStreamStub.partnerDeleteV1];
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer((_) async {
|
|
|
|
|
when(() => cancellationChecker()).thenReturn(true);
|
|
|
|
|
});
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
await handleEventsCallback(events, mockAbortCallbackWrapper.call);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
verify(() => mockSyncStreamRepo.deleteUsersV1(any())).called(1);
|
|
|
|
|
verifyNever(() => mockSyncStreamRepo.updateUsersV1(any()));
|
|
|
|
|
verifyNever(() => mockSyncStreamRepo.deletePartnerV1(any()));
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
verify(() => mockAbortCallbackWrapper()).called(1);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-04-18 14:01:16 -05:00
|
|
|
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
|
2025-04-17 20:55:27 +05:30
|
|
|
});
|
|
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
test("aborts and stops processing if cancelled before processing batch", () async {
|
|
|
|
|
final cancellationChecker = _MockCancellationWrapper();
|
|
|
|
|
when(() => cancellationChecker()).thenReturn(false);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
final processingCompleter = Completer<void>();
|
|
|
|
|
bool handler1Started = false;
|
|
|
|
|
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer((_) async {
|
|
|
|
|
handler1Started = true;
|
|
|
|
|
return processingCompleter.future;
|
|
|
|
|
});
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
sut = SyncStreamService(
|
|
|
|
|
syncApiRepository: mockSyncApiRepo,
|
|
|
|
|
syncStreamRepository: mockSyncStreamRepo,
|
2025-08-06 11:54:18 +03:00
|
|
|
trashService: mockTrashService,
|
2025-07-29 00:34:03 +05:30
|
|
|
cancelChecker: cancellationChecker.call,
|
|
|
|
|
);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
await sut.sync();
|
2025-04-18 14:01:16 -05:00
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
final events = [SyncStreamStub.userDeleteV1, SyncStreamStub.userV1Admin, SyncStreamStub.partnerDeleteV1];
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
final processingFuture = handleEventsCallback(events, mockAbortCallbackWrapper.call);
|
|
|
|
|
await pumpEventQueue();
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
expect(handler1Started, isTrue);
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
// Signal cancellation while handler 1 is waiting
|
|
|
|
|
when(() => cancellationChecker()).thenReturn(true);
|
|
|
|
|
await pumpEventQueue();
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
processingCompleter.complete();
|
|
|
|
|
await processingFuture;
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
verifyNever(() => mockSyncStreamRepo.updateUsersV1(any()));
|
2025-04-17 20:55:27 +05:30
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
|
|
|
|
|
});
|
2025-07-02 14:18:37 -05:00
|
|
|
|
|
|
|
|
test("processes memory sync events successfully", () async {
|
|
|
|
|
final events = [
|
|
|
|
|
SyncStreamStub.memoryV1,
|
|
|
|
|
SyncStreamStub.memoryDeleteV1,
|
|
|
|
|
SyncStreamStub.memoryToAssetV1,
|
|
|
|
|
SyncStreamStub.memoryToAssetDeleteV1,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
await simulateEvents(events);
|
|
|
|
|
|
|
|
|
|
verifyInOrder([
|
|
|
|
|
() => mockSyncStreamRepo.updateMemoriesV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["5"]),
|
|
|
|
|
() => mockSyncStreamRepo.deleteMemoriesV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["6"]),
|
|
|
|
|
() => mockSyncStreamRepo.updateMemoryAssetsV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["7"]),
|
|
|
|
|
() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["8"]),
|
|
|
|
|
]);
|
|
|
|
|
verifyNever(() => mockAbortCallbackWrapper());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("processes mixed memory and user events in correct order", () async {
|
|
|
|
|
final events = [
|
|
|
|
|
SyncStreamStub.memoryDeleteV1,
|
|
|
|
|
SyncStreamStub.userV1Admin,
|
|
|
|
|
SyncStreamStub.memoryToAssetV1,
|
|
|
|
|
SyncStreamStub.memoryV1,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
await simulateEvents(events);
|
|
|
|
|
|
|
|
|
|
verifyInOrder([
|
|
|
|
|
() => mockSyncStreamRepo.deleteMemoriesV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["6"]),
|
|
|
|
|
() => mockSyncStreamRepo.updateUsersV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["1"]),
|
|
|
|
|
() => mockSyncStreamRepo.updateMemoryAssetsV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["7"]),
|
|
|
|
|
() => mockSyncStreamRepo.updateMemoriesV1(any()),
|
|
|
|
|
() => mockSyncApiRepo.ack(["5"]),
|
|
|
|
|
]);
|
|
|
|
|
verifyNever(() => mockAbortCallbackWrapper());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("handles memory sync failure gracefully", () async {
|
2025-07-25 08:07:22 +05:30
|
|
|
when(() => mockSyncStreamRepo.updateMemoriesV1(any())).thenThrow(Exception("Memory sync failed"));
|
2025-07-02 14:18:37 -05:00
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
final events = [SyncStreamStub.memoryV1, SyncStreamStub.userV1Admin];
|
2025-07-02 14:18:37 -05:00
|
|
|
|
2025-07-29 00:34:03 +05:30
|
|
|
expect(() async => await simulateEvents(events), throwsA(isA<Exception>()));
|
2025-07-02 14:18:37 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("processes memory asset events with correct data types", () async {
|
|
|
|
|
final events = [SyncStreamStub.memoryToAssetV1];
|
|
|
|
|
|
|
|
|
|
await simulateEvents(events);
|
|
|
|
|
|
|
|
|
|
verify(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).called(1);
|
|
|
|
|
verify(() => mockSyncApiRepo.ack(["7"])).called(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("processes memory delete events with correct data types", () async {
|
|
|
|
|
final events = [SyncStreamStub.memoryDeleteV1];
|
|
|
|
|
|
|
|
|
|
await simulateEvents(events);
|
|
|
|
|
|
|
|
|
|
verify(() => mockSyncStreamRepo.deleteMemoriesV1(any())).called(1);
|
|
|
|
|
verify(() => mockSyncApiRepo.ack(["6"])).called(1);
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-25 08:07:22 +05:30
|
|
|
test("processes memory create/update events with correct data types", () async {
|
2025-07-02 14:18:37 -05:00
|
|
|
final events = [SyncStreamStub.memoryV1];
|
|
|
|
|
|
|
|
|
|
await simulateEvents(events);
|
|
|
|
|
|
|
|
|
|
verify(() => mockSyncStreamRepo.updateMemoriesV1(any())).called(1);
|
|
|
|
|
verify(() => mockSyncApiRepo.ack(["5"])).called(1);
|
|
|
|
|
});
|
2025-04-17 20:55:27 +05:30
|
|
|
});
|
|
|
|
|
}
|