Skip to content

Commit 3b7bf76

Browse files
chore: library unit tests (#13357)
1 parent 930df46 commit 3b7bf76

File tree

4 files changed

+152
-13
lines changed

4 files changed

+152
-13
lines changed

e2e/src/api/specs/library.e2e-spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,13 +500,13 @@ describe('/libraries', () => {
500500
});
501501

502502
it('should set an asset offline its file is not in any import path', async () => {
503+
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
504+
503505
const library = await utils.createLibrary(admin.accessToken, {
504506
ownerId: admin.userId,
505507
importPaths: [`${testAssetDirInternal}/temp/offline`],
506508
});
507509

508-
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
509-
510510
await scan(admin.accessToken, library.id);
511511
await utils.waitForQueueFinish(admin.accessToken, 'library');
512512

e2e/src/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,8 @@ export const utils = {
374374
},
375375

376376
createDirectory: (path: string) => {
377-
if (!existsSync(dirname(path))) {
378-
mkdirSync(dirname(path), { recursive: true });
377+
if (!existsSync(path)) {
378+
mkdirSync(path, { recursive: true });
379379
}
380380
},
381381

@@ -392,7 +392,7 @@ export const utils = {
392392
return;
393393
}
394394

395-
rmSync(path);
395+
rmSync(path, { recursive: true });
396396
},
397397

398398
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),

server/src/services/library.service.spec.ts

Lines changed: 146 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,64 @@ describe(LibraryService.name, () => {
119119
});
120120
});
121121

122+
describe('onConfigUpdateEvent', () => {
123+
beforeEach(async () => {
124+
systemMock.get.mockResolvedValue(defaults);
125+
databaseMock.tryLock.mockResolvedValue(true);
126+
await sut.onBootstrap();
127+
});
128+
129+
it('should do nothing if oldConfig is not provided', async () => {
130+
await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig });
131+
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
132+
});
133+
134+
it('should do nothing if instance does not have the watch lock', async () => {
135+
databaseMock.tryLock.mockResolvedValue(false);
136+
await sut.onBootstrap();
137+
await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig, oldConfig: defaults });
138+
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
139+
});
140+
141+
it('should update cron job and enable watching', async () => {
142+
libraryMock.getAll.mockResolvedValue([]);
143+
await sut.onConfigUpdate({
144+
newConfig: {
145+
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchEnabled.library },
146+
} as SystemConfig,
147+
oldConfig: defaults,
148+
});
149+
150+
expect(jobMock.updateCronJob).toHaveBeenCalledWith(
151+
'libraryScan',
152+
systemConfigStub.libraryScan.library.scan.cronExpression,
153+
systemConfigStub.libraryScan.library.scan.enabled,
154+
);
155+
});
156+
157+
it('should update cron job and disable watching', async () => {
158+
libraryMock.getAll.mockResolvedValue([]);
159+
await sut.onConfigUpdate({
160+
newConfig: {
161+
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchEnabled.library },
162+
} as SystemConfig,
163+
oldConfig: defaults,
164+
});
165+
await sut.onConfigUpdate({
166+
newConfig: {
167+
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchDisabled.library },
168+
} as SystemConfig,
169+
oldConfig: defaults,
170+
});
171+
172+
expect(jobMock.updateCronJob).toHaveBeenCalledWith(
173+
'libraryScan',
174+
systemConfigStub.libraryScan.library.scan.cronExpression,
175+
systemConfigStub.libraryScan.library.scan.enabled,
176+
);
177+
});
178+
});
179+
122180
describe('onConfigValidateEvent', () => {
123181
it('should allow a valid cron expression', () => {
124182
expect(() =>
@@ -139,7 +197,7 @@ describe(LibraryService.name, () => {
139197
});
140198
});
141199

142-
describe('handleQueueAssetRefresh', () => {
200+
describe('handleQueueSyncFiles', () => {
143201
it('should queue refresh of a new asset', async () => {
144202
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
145203
storageMock.walk.mockImplementation(mockWalk);
@@ -559,8 +617,8 @@ describe(LibraryService.name, () => {
559617
expect(jobMock.queueAll).not.toHaveBeenCalled();
560618
});
561619

562-
it('should throw BadRequestException when asset does not exist', async () => {
563-
storageMock.stat.mockRejectedValue(new Error("ENOENT, no such file or directory '/data/user1/photo.jpg'"));
620+
it('should fail when the file could not be read', async () => {
621+
storageMock.stat.mockRejectedValue(new Error('Could not read file'));
564622

565623
const mockLibraryJob: ILibraryFileJob = {
566624
id: libraryStub.externalLibrary1.id,
@@ -572,6 +630,27 @@ describe(LibraryService.name, () => {
572630
assetMock.create.mockResolvedValue(assetStub.image);
573631

574632
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
633+
expect(libraryMock.get).not.toHaveBeenCalled();
634+
expect(assetMock.create).not.toHaveBeenCalled();
635+
});
636+
637+
it('should skip if the file could not be found', async () => {
638+
const error = new Error('File not found') as any;
639+
error.code = 'ENOENT';
640+
storageMock.stat.mockRejectedValue(error);
641+
642+
const mockLibraryJob: ILibraryFileJob = {
643+
id: libraryStub.externalLibrary1.id,
644+
ownerId: userStub.admin.id,
645+
assetPath: '/data/user1/photo.jpg',
646+
};
647+
648+
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
649+
assetMock.create.mockResolvedValue(assetStub.image);
650+
651+
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
652+
expect(libraryMock.get).not.toHaveBeenCalled();
653+
expect(assetMock.create).not.toHaveBeenCalled();
575654
});
576655
});
577656

@@ -654,6 +733,10 @@ describe(LibraryService.name, () => {
654733

655734
expect(libraryMock.getStatistics).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
656735
});
736+
737+
it('should throw an error if the library could not be found', async () => {
738+
await expect(sut.getStatistics('foo')).rejects.toBeInstanceOf(BadRequestException);
739+
});
657740
});
658741

659742
describe('create', () => {
@@ -783,6 +866,13 @@ describe(LibraryService.name, () => {
783866
});
784867
});
785868

869+
describe('getAll', () => {
870+
it('should get all libraries', async () => {
871+
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
872+
await expect(sut.getAll()).resolves.toEqual([expect.objectContaining({ id: libraryStub.externalLibrary1.id })]);
873+
});
874+
});
875+
786876
describe('handleQueueCleanup', () => {
787877
it('should queue cleanup jobs', async () => {
788878
libraryMock.getAllDeleted.mockResolvedValue([libraryStub.externalLibrary1, libraryStub.externalLibrary2]);
@@ -803,15 +893,38 @@ describe(LibraryService.name, () => {
803893
await sut.onBootstrap();
804894
});
805895

896+
it('should throw an error if an import path is invalid', async () => {
897+
libraryMock.update.mockResolvedValue(libraryStub.externalLibrary1);
898+
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
899+
900+
await expect(sut.update('library-id', { importPaths: ['foo/bar'] })).rejects.toBeInstanceOf(BadRequestException);
901+
expect(libraryMock.update).not.toHaveBeenCalled();
902+
});
903+
806904
it('should update library', async () => {
807905
libraryMock.update.mockResolvedValue(libraryStub.externalLibrary1);
808906
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
809-
await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.externalLibrary1));
907+
storageMock.stat.mockResolvedValue({ isDirectory: () => true } as Stats);
908+
storageMock.checkFileExists.mockResolvedValue(true);
909+
910+
await expect(sut.update('library-id', { importPaths: ['foo/bar'] })).resolves.toEqual(
911+
mapLibrary(libraryStub.externalLibrary1),
912+
);
810913
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
811914
});
812915
});
813916

917+
describe('onShutdown', () => {
918+
it('should do nothing if instance does not have the watch lock', async () => {
919+
await sut.onShutdown();
920+
});
921+
});
922+
814923
describe('watchAll', () => {
924+
it('should return false if instance does not have the watch lock', async () => {
925+
await expect(sut.watchAll()).resolves.toBe(false);
926+
});
927+
815928
describe('watching disabled', () => {
816929
beforeEach(async () => {
817930
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
@@ -872,6 +985,7 @@ describe(LibraryService.name, () => {
872985
it('should handle a new file event', async () => {
873986
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
874987
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
988+
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
875989
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
876990

877991
await sut.watchAll();
@@ -886,11 +1000,15 @@ describe(LibraryService.name, () => {
8861000
},
8871001
},
8881002
]);
1003+
expect(jobMock.queueAll).toHaveBeenCalledWith([
1004+
{ name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
1005+
]);
8891006
});
8901007

8911008
it('should handle a file change event', async () => {
8921009
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
8931010
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
1011+
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
8941012
storageMock.watch.mockImplementation(
8951013
makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }),
8961014
);
@@ -907,6 +1025,24 @@ describe(LibraryService.name, () => {
9071025
},
9081026
},
9091027
]);
1028+
expect(jobMock.queueAll).toHaveBeenCalledWith([
1029+
{ name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
1030+
]);
1031+
});
1032+
1033+
it('should handle a file unlink event', async () => {
1034+
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
1035+
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
1036+
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
1037+
storageMock.watch.mockImplementation(
1038+
makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }),
1039+
);
1040+
1041+
await sut.watchAll();
1042+
1043+
expect(jobMock.queueAll).toHaveBeenCalledWith([
1044+
{ name: JobName.LIBRARY_SYNC_ASSET, data: expect.objectContaining({ id: assetStub.image.id }) },
1045+
]);
9101046
});
9111047

9121048
it('should handle an error event', async () => {
@@ -986,15 +1122,14 @@ describe(LibraryService.name, () => {
9861122
it('should delete an empty library', async () => {
9871123
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
9881124
assetMock.getAll.mockResolvedValue({ items: [], hasNextPage: false });
989-
libraryMock.delete.mockImplementation(async () => {});
9901125

9911126
await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
1127+
expect(libraryMock.delete).toHaveBeenCalled();
9921128
});
9931129

994-
it('should delete a library with assets', async () => {
1130+
it('should delete all assets in a library', async () => {
9951131
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
9961132
assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
997-
libraryMock.delete.mockImplementation(async () => {});
9981133

9991134
assetMock.getById.mockResolvedValue(assetStub.image1);
10001135

@@ -1076,6 +1211,10 @@ describe(LibraryService.name, () => {
10761211
});
10771212

10781213
describe('validate', () => {
1214+
it('should not require import paths', async () => {
1215+
await expect(sut.validate('library-id', {})).resolves.toEqual({ importPaths: [] });
1216+
});
1217+
10791218
it('should validate directory', async () => {
10801219
storageMock.stat.mockResolvedValue({
10811220
isDirectory: () => true,

server/src/services/library.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,6 @@ export class LibraryService extends BaseService {
303303

304304
async update(id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
305305
await this.findOrFail(id);
306-
const library = await this.libraryRepository.update({ id, ...dto });
307306

308307
if (dto.importPaths) {
309308
const validation = await this.validate(id, { importPaths: dto.importPaths });
@@ -316,6 +315,7 @@ export class LibraryService extends BaseService {
316315
}
317316
}
318317

318+
const library = await this.libraryRepository.update({ id, ...dto });
319319
return mapLibrary(library);
320320
}
321321

0 commit comments

Comments
 (0)