mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
chore: library unit tests (#13357)
This commit is contained in:
parent
930df46f74
commit
3b7bf76db9
@ -500,13 +500,13 @@ describe('/libraries', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set an asset offline its file is not in any import path', async () => {
|
it('should set an asset offline its file is not in any import path', async () => {
|
||||||
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
|
||||||
|
@ -374,8 +374,8 @@ export const utils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
createDirectory: (path: string) => {
|
createDirectory: (path: string) => {
|
||||||
if (!existsSync(dirname(path))) {
|
if (!existsSync(path)) {
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(path, { recursive: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -392,7 +392,7 @@ export const utils = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rmSync(path);
|
rmSync(path, { recursive: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
@ -119,6 +119,64 @@ describe(LibraryService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onConfigUpdateEvent', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
systemMock.get.mockResolvedValue(defaults);
|
||||||
|
databaseMock.tryLock.mockResolvedValue(true);
|
||||||
|
await sut.onBootstrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if oldConfig is not provided', async () => {
|
||||||
|
await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig });
|
||||||
|
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if instance does not have the watch lock', async () => {
|
||||||
|
databaseMock.tryLock.mockResolvedValue(false);
|
||||||
|
await sut.onBootstrap();
|
||||||
|
await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig, oldConfig: defaults });
|
||||||
|
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cron job and enable watching', async () => {
|
||||||
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
|
await sut.onConfigUpdate({
|
||||||
|
newConfig: {
|
||||||
|
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchEnabled.library },
|
||||||
|
} as SystemConfig,
|
||||||
|
oldConfig: defaults,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(jobMock.updateCronJob).toHaveBeenCalledWith(
|
||||||
|
'libraryScan',
|
||||||
|
systemConfigStub.libraryScan.library.scan.cronExpression,
|
||||||
|
systemConfigStub.libraryScan.library.scan.enabled,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cron job and disable watching', async () => {
|
||||||
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
|
await sut.onConfigUpdate({
|
||||||
|
newConfig: {
|
||||||
|
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchEnabled.library },
|
||||||
|
} as SystemConfig,
|
||||||
|
oldConfig: defaults,
|
||||||
|
});
|
||||||
|
await sut.onConfigUpdate({
|
||||||
|
newConfig: {
|
||||||
|
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchDisabled.library },
|
||||||
|
} as SystemConfig,
|
||||||
|
oldConfig: defaults,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(jobMock.updateCronJob).toHaveBeenCalledWith(
|
||||||
|
'libraryScan',
|
||||||
|
systemConfigStub.libraryScan.library.scan.cronExpression,
|
||||||
|
systemConfigStub.libraryScan.library.scan.enabled,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('onConfigValidateEvent', () => {
|
describe('onConfigValidateEvent', () => {
|
||||||
it('should allow a valid cron expression', () => {
|
it('should allow a valid cron expression', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
@ -139,7 +197,7 @@ describe(LibraryService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleQueueAssetRefresh', () => {
|
describe('handleQueueSyncFiles', () => {
|
||||||
it('should queue refresh of a new asset', async () => {
|
it('should queue refresh of a new asset', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
storageMock.walk.mockImplementation(mockWalk);
|
storageMock.walk.mockImplementation(mockWalk);
|
||||||
@ -559,8 +617,8 @@ describe(LibraryService.name, () => {
|
|||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw BadRequestException when asset does not exist', async () => {
|
it('should fail when the file could not be read', async () => {
|
||||||
storageMock.stat.mockRejectedValue(new Error("ENOENT, no such file or directory '/data/user1/photo.jpg'"));
|
storageMock.stat.mockRejectedValue(new Error('Could not read file'));
|
||||||
|
|
||||||
const mockLibraryJob: ILibraryFileJob = {
|
const mockLibraryJob: ILibraryFileJob = {
|
||||||
id: libraryStub.externalLibrary1.id,
|
id: libraryStub.externalLibrary1.id,
|
||||||
@ -572,6 +630,27 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
|
||||||
|
expect(libraryMock.get).not.toHaveBeenCalled();
|
||||||
|
expect(assetMock.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip if the file could not be found', async () => {
|
||||||
|
const error = new Error('File not found') as any;
|
||||||
|
error.code = 'ENOENT';
|
||||||
|
storageMock.stat.mockRejectedValue(error);
|
||||||
|
|
||||||
|
const mockLibraryJob: ILibraryFileJob = {
|
||||||
|
id: libraryStub.externalLibrary1.id,
|
||||||
|
ownerId: userStub.admin.id,
|
||||||
|
assetPath: '/data/user1/photo.jpg',
|
||||||
|
};
|
||||||
|
|
||||||
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
||||||
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
|
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
|
||||||
|
expect(libraryMock.get).not.toHaveBeenCalled();
|
||||||
|
expect(assetMock.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -654,6 +733,10 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
expect(libraryMock.getStatistics).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
|
expect(libraryMock.getStatistics).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the library could not be found', async () => {
|
||||||
|
await expect(sut.getStatistics('foo')).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
@ -783,6 +866,13 @@ describe(LibraryService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should get all libraries', async () => {
|
||||||
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
||||||
|
await expect(sut.getAll()).resolves.toEqual([expect.objectContaining({ id: libraryStub.externalLibrary1.id })]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('handleQueueCleanup', () => {
|
describe('handleQueueCleanup', () => {
|
||||||
it('should queue cleanup jobs', async () => {
|
it('should queue cleanup jobs', async () => {
|
||||||
libraryMock.getAllDeleted.mockResolvedValue([libraryStub.externalLibrary1, libraryStub.externalLibrary2]);
|
libraryMock.getAllDeleted.mockResolvedValue([libraryStub.externalLibrary1, libraryStub.externalLibrary2]);
|
||||||
@ -803,15 +893,38 @@ describe(LibraryService.name, () => {
|
|||||||
await sut.onBootstrap();
|
await sut.onBootstrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error if an import path is invalid', async () => {
|
||||||
|
libraryMock.update.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
|
|
||||||
|
await expect(sut.update('library-id', { importPaths: ['foo/bar'] })).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
expect(libraryMock.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update library', async () => {
|
it('should update library', async () => {
|
||||||
libraryMock.update.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.update.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.externalLibrary1));
|
storageMock.stat.mockResolvedValue({ isDirectory: () => true } as Stats);
|
||||||
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await expect(sut.update('library-id', { importPaths: ['foo/bar'] })).resolves.toEqual(
|
||||||
|
mapLibrary(libraryStub.externalLibrary1),
|
||||||
|
);
|
||||||
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onShutdown', () => {
|
||||||
|
it('should do nothing if instance does not have the watch lock', async () => {
|
||||||
|
await sut.onShutdown();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('watchAll', () => {
|
describe('watchAll', () => {
|
||||||
|
it('should return false if instance does not have the watch lock', async () => {
|
||||||
|
await expect(sut.watchAll()).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
describe('watching disabled', () => {
|
describe('watching disabled', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||||
@ -872,6 +985,7 @@ 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]);
|
||||||
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||||
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
|
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
|
||||||
|
|
||||||
await sut.watchAll();
|
await sut.watchAll();
|
||||||
@ -886,11 +1000,15 @@ describe(LibraryService.name, () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
|
{ name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a file change event', async () => {
|
it('should handle a file change event', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||||
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||||
storageMock.watch.mockImplementation(
|
storageMock.watch.mockImplementation(
|
||||||
makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }),
|
makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }),
|
||||||
);
|
);
|
||||||
@ -907,6 +1025,24 @@ describe(LibraryService.name, () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
|
{ name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a file unlink event', async () => {
|
||||||
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||||
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||||
|
storageMock.watch.mockImplementation(
|
||||||
|
makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.watchAll();
|
||||||
|
|
||||||
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
|
{ name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle an error event', async () => {
|
it('should handle an error event', async () => {
|
||||||
@ -986,15 +1122,14 @@ describe(LibraryService.name, () => {
|
|||||||
it('should delete an empty library', async () => {
|
it('should delete an empty library', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
assetMock.getAll.mockResolvedValue({ items: [], hasNextPage: false });
|
assetMock.getAll.mockResolvedValue({ items: [], hasNextPage: false });
|
||||||
libraryMock.delete.mockImplementation(async () => {});
|
|
||||||
|
|
||||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
expect(libraryMock.delete).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete a library with assets', async () => {
|
it('should delete all assets in a library', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
|
assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
|
||||||
libraryMock.delete.mockImplementation(async () => {});
|
|
||||||
|
|
||||||
assetMock.getById.mockResolvedValue(assetStub.image1);
|
assetMock.getById.mockResolvedValue(assetStub.image1);
|
||||||
|
|
||||||
@ -1076,6 +1211,10 @@ describe(LibraryService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('validate', () => {
|
describe('validate', () => {
|
||||||
|
it('should not require import paths', async () => {
|
||||||
|
await expect(sut.validate('library-id', {})).resolves.toEqual({ importPaths: [] });
|
||||||
|
});
|
||||||
|
|
||||||
it('should validate directory', async () => {
|
it('should validate directory', async () => {
|
||||||
storageMock.stat.mockResolvedValue({
|
storageMock.stat.mockResolvedValue({
|
||||||
isDirectory: () => true,
|
isDirectory: () => true,
|
||||||
|
@ -303,7 +303,6 @@ export class LibraryService extends BaseService {
|
|||||||
|
|
||||||
async update(id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
|
async update(id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
|
||||||
await this.findOrFail(id);
|
await this.findOrFail(id);
|
||||||
const library = await this.libraryRepository.update({ id, ...dto });
|
|
||||||
|
|
||||||
if (dto.importPaths) {
|
if (dto.importPaths) {
|
||||||
const validation = await this.validate(id, { importPaths: dto.importPaths });
|
const validation = await this.validate(id, { importPaths: dto.importPaths });
|
||||||
@ -316,6 +315,7 @@ export class LibraryService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const library = await this.libraryRepository.update({ id, ...dto });
|
||||||
return mapLibrary(library);
|
return mapLibrary(library);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user