refactor event type mocks

This commit is contained in:
Jonathan Jogenfors
2024-02-29 23:36:05 +01:00
parent 0f331a3b50
commit 3d9c677c4a
7 changed files with 60 additions and 41 deletions
@@ -1,4 +1,4 @@
import { LibraryResponseDto, LibraryService, LoginResponseDto, StorageEvent } from '@app/domain'; import { LibraryResponseDto, LibraryService, LoginResponseDto, StorageEventType } from '@app/domain';
import { AssetType, LibraryType } from '@app/infra/entities'; import { AssetType, LibraryType } from '@app/infra/entities';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
@@ -57,7 +57,7 @@ describe(`Library watcher (e2e)`, () => {
`${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`,
); );
await waitForEvent(libraryService, StorageEvent.ADD); await waitForEvent(libraryService, StorageEventType.ADD);
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(afterAssets.length).toEqual(1); expect(afterAssets.length).toEqual(1);
@@ -84,7 +84,7 @@ describe(`Library watcher (e2e)`, () => {
`${IMMICH_TEST_ASSET_TEMP_PATH}/file5.jPg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file5.jPg`,
); );
await waitForEvent(libraryService, StorageEvent.ADD, 4); await waitForEvent(libraryService, StorageEventType.ADD, 4);
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(afterAssets.length).toEqual(4); expect(afterAssets.length).toEqual(4);
@@ -96,7 +96,7 @@ describe(`Library watcher (e2e)`, () => {
`${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`,
); );
await waitForEvent(libraryService, StorageEvent.ADD); await waitForEvent(libraryService, StorageEventType.ADD);
const originalAssets = await api.assetApi.getAllAssets(server, admin.accessToken); const originalAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(originalAssets.length).toEqual(1); expect(originalAssets.length).toEqual(1);
@@ -106,7 +106,7 @@ describe(`Library watcher (e2e)`, () => {
`${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`,
); );
await waitForEvent(libraryService, StorageEvent.CHANGE); await waitForEvent(libraryService, StorageEventType.CHANGE);
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(afterAssets).toEqual([ expect(afterAssets).toEqual([
@@ -158,7 +158,7 @@ describe(`Library watcher (e2e)`, () => {
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3/file4.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3/file4.jpg`,
); );
await waitForEvent(libraryService, StorageEvent.ADD, 3); await waitForEvent(libraryService, StorageEventType.ADD, 3);
const assets = await api.assetApi.getAllAssets(server, admin.accessToken); const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(assets.length).toEqual(3); expect(assets.length).toEqual(3);
@@ -170,14 +170,14 @@ describe(`Library watcher (e2e)`, () => {
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`,
); );
await waitForEvent(libraryService, StorageEvent.ADD); await waitForEvent(libraryService, StorageEventType.ADD);
const addedAssets = await api.assetApi.getAllAssets(server, admin.accessToken); const addedAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(addedAssets.length).toEqual(1); expect(addedAssets.length).toEqual(1);
await fs.unlink(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`); await fs.unlink(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`);
await waitForEvent(libraryService, StorageEvent.UNLINK); await waitForEvent(libraryService, StorageEventType.UNLINK);
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(afterAssets[0].isOffline).toEqual(true); expect(afterAssets[0].isOffline).toEqual(true);
@@ -29,6 +29,7 @@ import {
IStorageRepository, IStorageRepository,
ISystemConfigRepository, ISystemConfigRepository,
IUserRepository, IUserRepository,
StorageEventType,
} from '../repositories'; } from '../repositories';
import { SystemConfigCore } from '../system-config/system-config.core'; import { SystemConfigCore } from '../system-config/system-config.core';
import { mapLibrary } from './library.dto'; import { mapLibrary } from './library.dto';
@@ -1187,7 +1188,9 @@ describe(LibraryService.name, () => {
it('should handle a new file event', async () => { it('should handle a new file event', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); storageMock.watch.mockImplementation(
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }),
);
await sut.watchAll(); await sut.watchAll();
@@ -1208,7 +1211,7 @@ describe(LibraryService.name, () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
storageMock.watch.mockImplementation( storageMock.watch.mockImplementation(
makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }), makeMockWatcher({ items: [{ event: StorageEventType.CHANGE, value: '/foo/photo.jpg' }] }),
); );
await sut.watchAll(); await sut.watchAll();
@@ -1231,7 +1234,7 @@ describe(LibraryService.name, () => {
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
storageMock.watch.mockImplementation( storageMock.watch.mockImplementation(
makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }), makeMockWatcher({ items: [{ event: StorageEventType.UNLINK, value: '/foo/photo.jpg' }] }),
); );
await sut.watchAll(); await sut.watchAll();
@@ -1245,17 +1248,19 @@ describe(LibraryService.name, () => {
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
storageMock.watch.mockImplementation( storageMock.watch.mockImplementation(
makeMockWatcher({ makeMockWatcher({
items: [{ event: 'error', value: 'Error!' }], items: [{ event: StorageEventType.ERROR, value: 'Error!' }],
}), }),
); );
await sut.watchAll(); await expect(sut.watchAll()).rejects.toEqual([expect.stringMatching('/Error: Error/')]);
}); });
it('should ignore unknown extensions', async () => { it('should ignore unknown extensions', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); storageMock.watch.mockImplementation(
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }),
);
await sut.watchAll(); await sut.watchAll();
@@ -1265,7 +1270,9 @@ describe(LibraryService.name, () => {
it('should ignore excluded paths', async () => { it('should ignore excluded paths', async () => {
libraryMock.get.mockResolvedValue(libraryStub.patternPath); libraryMock.get.mockResolvedValue(libraryStub.patternPath);
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]); libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/dir1/photo.txt' }] })); storageMock.watch.mockImplementation(
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/dir1/photo.txt' }] }),
);
await sut.watchAll(); await sut.watchAll();
@@ -1275,7 +1282,9 @@ describe(LibraryService.name, () => {
it('should ignore excluded paths without case sensitivity', async () => { it('should ignore excluded paths without case sensitivity', async () => {
libraryMock.get.mockResolvedValue(libraryStub.patternPath); libraryMock.get.mockResolvedValue(libraryStub.patternPath);
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]); libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/DIR1/photo.txt' }] })); storageMock.watch.mockImplementation(
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/DIR1/photo.txt' }] }),
);
await sut.watchAll(); await sut.watchAll();
@@ -1284,7 +1293,7 @@ describe(LibraryService.name, () => {
}); });
}); });
describe('tearDown', () => { describe('teardown', () => {
it('should tear down all watchers', async () => { it('should tear down all watchers', async () => {
libraryMock.getAll.mockResolvedValue([ libraryMock.getAll.mockResolvedValue([
libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths1,
+10 -8
View File
@@ -23,7 +23,7 @@ import {
IStorageRepository, IStorageRepository,
ISystemConfigRepository, ISystemConfigRepository,
IUserRepository, IUserRepository,
StorageEvent, StorageEventType,
WithProperty, WithProperty,
} from '../repositories'; } from '../repositories';
import { SystemConfigCore } from '../system-config'; import { SystemConfigCore } from '../system-config';
@@ -97,9 +97,11 @@ export class LibraryService extends EventEmitter {
this.configCore.config$.subscribe(async ({ library }) => { this.configCore.config$.subscribe(async ({ library }) => {
this.jobRepository.updateCronJob('libraryScan', library.scan.cronExpression, library.scan.enabled); this.jobRepository.updateCronJob('libraryScan', library.scan.cronExpression, library.scan.enabled);
if (this.watchLock && library.watch.enabled !== this.watchLibraries) { if (this.watchLock) {
this.watchLibraries = library.watch.enabled; if (library.watch.enabled !== this.watchLibraries) {
await (this.watchLibraries ? this.watchAll() : this.unwatchAll()); this.watchLibraries = library.watch.enabled;
await (this.watchLibraries ? this.watchAll() : this.unwatchAll());
}
} }
}); });
} }
@@ -142,7 +144,7 @@ export class LibraryService extends EventEmitter {
if (matcher(path)) { if (matcher(path)) {
await this.scanAssets(library.id, [path], library.ownerId, false); await this.scanAssets(library.id, [path], library.ownerId, false);
} }
this.emit(StorageEvent.ADD, path); this.emit(StorageEventType.ADD, path);
}, },
onChange: async (path) => { onChange: async (path) => {
this.logger.debug(`Detected file change for ${path} in library ${library.id}`); this.logger.debug(`Detected file change for ${path} in library ${library.id}`);
@@ -150,7 +152,7 @@ export class LibraryService extends EventEmitter {
// Note: if the changed file was not previously imported, it will be imported now. // Note: if the changed file was not previously imported, it will be imported now.
await this.scanAssets(library.id, [path], library.ownerId, false); await this.scanAssets(library.id, [path], library.ownerId, false);
} }
this.emit(StorageEvent.CHANGE, path); this.emit(StorageEventType.CHANGE, path);
}, },
onUnlink: async (path) => { onUnlink: async (path) => {
this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`); this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`);
@@ -158,12 +160,12 @@ export class LibraryService extends EventEmitter {
if (asset && matcher(path)) { if (asset && matcher(path)) {
await this.assetRepository.save({ id: asset.id, isOffline: true }); await this.assetRepository.save({ id: asset.id, isOffline: true });
} }
this.emit(StorageEvent.UNLINK, path); this.emit(StorageEventType.UNLINK, path);
}, },
onError: (error) => { onError: (error) => {
// TODO: should we log, or throw an exception? // TODO: should we log, or throw an exception?
this.logger.error(`Library watcher for library ${library.id} encountered error: ${error}`); this.logger.error(`Library watcher for library ${library.id} encountered error: ${error}`);
this.emit(StorageEvent.ERROR, error); this.emit(StorageEventType.ERROR, error);
}, },
}, },
); );
@@ -31,7 +31,7 @@ export interface WatchEvents {
onError(error: Error): void; onError(error: Error): void;
} }
export enum StorageEvent { export enum StorageEventType {
READY = 'ready', READY = 'ready',
ADD = 'add', ADD = 'add',
CHANGE = 'change', CHANGE = 'change',
@@ -4,7 +4,7 @@ import {
IStorageRepository, IStorageRepository,
ImmichReadStream, ImmichReadStream,
ImmichZipStream, ImmichZipStream,
StorageEvent, StorageEventType,
WatchEvents, WatchEvents,
mimeTypes, mimeTypes,
} from '@app/domain'; } from '@app/domain';
@@ -142,11 +142,11 @@ export class FilesystemProvider implements IStorageRepository {
watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>) { watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>) {
const watcher = chokidar.watch(paths, options); const watcher = chokidar.watch(paths, options);
watcher.on(StorageEvent.READY, () => events.onReady?.()); watcher.on(StorageEventType.READY, () => events.onReady?.());
watcher.on(StorageEvent.ADD, (path) => events.onAdd?.(path)); watcher.on(StorageEventType.ADD, (path) => events.onAdd?.(path));
watcher.on(StorageEvent.CHANGE, (path) => events.onChange?.(path)); watcher.on(StorageEventType.CHANGE, (path) => events.onChange?.(path));
watcher.on(StorageEvent.UNLINK, (path) => events.onUnlink?.(path)); watcher.on(StorageEventType.UNLINK, (path) => events.onUnlink?.(path));
watcher.on(StorageEvent.ERROR, (error) => events.onError?.(error)); watcher.on(StorageEventType.ERROR, (error) => events.onError?.(error));
return () => watcher.close(); return () => watcher.close();
} }
+3 -3
View File
@@ -1,4 +1,4 @@
import { IJobRepository, IMediaRepository, JobItem, JobItemHandler, QueueName, StorageEvent } from '@app/domain'; import { IJobRepository, IMediaRepository, JobItem, JobItemHandler, QueueName, StorageEventType } from '@app/domain';
import { AppModule } from '@app/immich'; import { AppModule } from '@app/immich';
import { InfraModule, InfraTestModule, dataSource } from '@app/infra'; import { InfraModule, InfraTestModule, dataSource } from '@app/infra';
import { MediaRepository } from '@app/infra/repositories'; import { MediaRepository } from '@app/infra/repositories';
@@ -145,7 +145,7 @@ export function waitForEvent(emitter: EventEmitter, event: string, times = 1): P
promises.push( promises.push(
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const success = (value: any) => { const success = (value: any) => {
emitter.off(StorageEvent.ERROR, fail); emitter.off(StorageEventType.ERROR, fail);
resolve(value); resolve(value);
}; };
const fail = (error: Error) => { const fail = (error: Error) => {
@@ -153,7 +153,7 @@ export function waitForEvent(emitter: EventEmitter, event: string, times = 1): P
reject(error); reject(error);
}; };
emitter.once(event, success); emitter.once(event, success);
emitter.once(StorageEvent.ERROR, fail); emitter.once(StorageEventType.ERROR, fail);
}), }),
); );
} }
@@ -1,8 +1,16 @@
import { IStorageRepository, StorageCore, WatchEvents } from '@app/domain'; import { IStorageRepository, StorageCore, StorageEventType, WatchEvents } from '@app/domain';
import { WatchOptions } from 'chokidar'; import { WatchOptions } from 'chokidar';
interface MockWatcherOptions { interface MockWatcherOptions {
items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>; items?: Array<{
event:
| StorageEventType.READY
| StorageEventType.ADD
| StorageEventType.CHANGE
| StorageEventType.UNLINK
| StorageEventType.ERROR;
value: string;
}>;
close?: () => void; close?: () => void;
} }
@@ -12,19 +20,19 @@ export const makeMockWatcher =
events.onReady?.(); events.onReady?.();
for (const item of items || []) { for (const item of items || []) {
switch (item.event) { switch (item.event) {
case 'add': { case StorageEventType.ADD: {
events.onAdd?.(item.value); events.onAdd?.(item.value);
break; break;
} }
case 'change': { case StorageEventType.CHANGE: {
events.onChange?.(item.value); events.onChange?.(item.value);
break; break;
} }
case 'unlink': { case StorageEventType.UNLINK: {
events.onUnlink?.(item.value); events.onUnlink?.(item.value);
break; break;
} }
case 'error': { case StorageEventType.ERROR: {
events.onError?.(new Error(item.value)); events.onError?.(new Error(item.value));
} }
} }