refactor: more small tests (#26159)

This commit is contained in:
Daniel Dietzler
2026-02-12 14:34:32 +01:00
committed by GitHub
parent 913904f418
commit 7e0356e227
10 changed files with 463 additions and 782 deletions

View File

@@ -7,11 +7,10 @@ import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum';
import { LibraryService } from 'src/services/library.service';
import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types';
import { AssetFactory } from 'test/factories/asset.factory';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { makeMockWatcher } from 'test/repositories/storage.repository.mock';
import { factory, newUuid } from 'test/small.factory';
import { factory, newDate, newUuid } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
import { vitest } from 'vitest';
@@ -307,13 +306,13 @@ describe(LibraryService.name, () => {
it('should queue asset sync', async () => {
const library = factory.library({ importPaths: ['/foo', '/bar'] });
const asset = AssetFactory.create({ libraryId: library.id, isExternal: true });
mocks.library.get.mockResolvedValue(library);
mocks.storage.walk.mockImplementation(async function* generator() {});
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external]));
mocks.library.streamAssetIds.mockReturnValue(makeStream([asset]));
mocks.asset.getLibraryAssetCount.mockResolvedValue(1);
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: 0n });
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external]));
const response = await sut.handleQueueSyncAssets({ id: library.id });
@@ -323,7 +322,7 @@ describe(LibraryService.name, () => {
libraryId: library.id,
importPaths: library.importPaths,
exclusionPatterns: library.exclusionPatterns,
assetIds: [assetStub.external.id],
assetIds: [asset.id],
progressCounter: 1,
totalAssets: 1,
},
@@ -344,8 +343,9 @@ describe(LibraryService.name, () => {
describe('handleSyncAssets', () => {
it('should offline assets no longer on disk', async () => {
const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.external.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/'],
exclusionPatterns: [],
@@ -353,20 +353,21 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockRejectedValue(new Error('ENOENT, no such file or directory'));
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], {
isOffline: true,
deletedAt: expect.anything(),
});
});
it('should set assets deleted from disk as offline', async () => {
const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.external.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/data/user2'],
exclusionPatterns: [],
@@ -374,20 +375,21 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockRejectedValue(new Error('Could not read file'));
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], {
isOffline: true,
deletedAt: expect.anything(),
});
});
it('should do nothing with offline assets deleted from disk', async () => {
const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.trashedOffline.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/data/user2'],
exclusionPatterns: [],
@@ -395,7 +397,7 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockRejectedValue(new Error('Could not read file'));
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
@@ -404,8 +406,9 @@ describe(LibraryService.name, () => {
});
it('should un-trash an asset previously marked as offline', async () => {
const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.trashedOffline.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/original/'],
exclusionPatterns: [],
@@ -413,20 +416,21 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats);
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], {
isOffline: false,
deletedAt: null,
});
});
it('should do nothing with offline asset if covered by exclusion pattern', async () => {
const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.trashedOffline.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/original/'],
exclusionPatterns: ['**/path.jpg'],
@@ -434,8 +438,8 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats);
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
@@ -445,8 +449,9 @@ describe(LibraryService.name, () => {
});
it('should do nothing with offline asset if not in import path', async () => {
const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.trashedOffline.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/import/'],
exclusionPatterns: [],
@@ -454,8 +459,8 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats);
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
@@ -465,8 +470,9 @@ describe(LibraryService.name, () => {
});
it('should do nothing with unchanged online assets', async () => {
const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.external.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/'],
exclusionPatterns: [],
@@ -474,8 +480,8 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockResolvedValue({ mtime: asset.fileModifiedAt } as Stats);
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
@@ -483,8 +489,9 @@ describe(LibraryService.name, () => {
});
it('should not touch fileCreatedAt when un-trashing an asset previously marked as offline', async () => {
const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.trashedOffline.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/'],
exclusionPatterns: [],
@@ -492,13 +499,13 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats);
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
expect(mocks.asset.updateAll).toHaveBeenCalledWith(
[assetStub.trashedOffline.id],
[asset.id],
expect.not.objectContaining({
fileCreatedAt: expect.anything(),
}),
@@ -506,8 +513,9 @@ describe(LibraryService.name, () => {
});
it('should update with online assets that have changed', async () => {
const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
const mockAssetJob: ILibraryBulkIdsJob = {
assetIds: [assetStub.external.id],
assetIds: [asset.id],
libraryId: newUuid(),
importPaths: ['/'],
exclusionPatterns: [],
@@ -515,13 +523,9 @@ describe(LibraryService.name, () => {
progressCounter: 0,
};
if (assetStub.external.fileModifiedAt == null) {
throw new Error('fileModifiedAt is null');
}
const mtime = new Date(asset.fileModifiedAt.getDate() + 1);
const mtime = new Date(assetStub.external.fileModifiedAt.getDate() + 1);
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
mocks.storage.stat.mockResolvedValue({ mtime } as Stats);
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
@@ -530,7 +534,7 @@ describe(LibraryService.name, () => {
{
name: JobName.SidecarCheck,
data: {
id: assetStub.external.id,
id: asset.id,
source: 'upload',
},
},
@@ -1023,9 +1027,10 @@ describe(LibraryService.name, () => {
it('should handle an error event', async () => {
const library = factory.library({ importPaths: ['/foo', '/bar'] });
const asset = AssetFactory.create({ libraryId: library.id, isExternal: true });
mocks.library.get.mockResolvedValue(library);
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(asset);
mocks.library.getAll.mockResolvedValue([library]);
mocks.storage.watch.mockImplementation(
makeMockWatcher({