2023-09-20 13:16:33 +02:00
|
|
|
import { BadRequestException } from '@nestjs/common';
|
2024-03-05 23:23:06 +01:00
|
|
|
import { when } from 'jest-when';
|
2024-03-15 23:01:58 +01:00
|
|
|
import { R_OK } from 'node:constants';
|
2024-02-02 04:18:00 +01:00
|
|
|
import { Stats } from 'node:fs';
|
2024-03-20 21:20:38 +01:00
|
|
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { JobName } from 'src/domain/job/job.constants';
|
|
|
|
import { ILibraryFileJob, ILibraryRefreshJob } from 'src/domain/job/job.interface';
|
|
|
|
import { mapLibrary } from 'src/domain/library/library.dto';
|
|
|
|
import { LibraryService } from 'src/domain/library/library.service';
|
2024-03-20 22:02:51 +01:00
|
|
|
import { AssetType } from 'src/entities/asset.entity';
|
|
|
|
import { LibraryType } from 'src/entities/library.entity';
|
|
|
|
import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity';
|
|
|
|
import { UserEntity } from 'src/entities/user.entity';
|
2024-03-20 21:42:58 +01:00
|
|
|
import { IAssetRepository } from 'src/interfaces/asset.repository';
|
|
|
|
import { ICryptoRepository } from 'src/interfaces/crypto.repository';
|
|
|
|
import { IDatabaseRepository } from 'src/interfaces/database.repository';
|
|
|
|
import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
|
|
|
|
import { ILibraryRepository } from 'src/interfaces/library.repository';
|
|
|
|
import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.repository';
|
|
|
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { assetStub } from 'test/fixtures/asset.stub';
|
|
|
|
import { authStub } from 'test/fixtures/auth.stub';
|
|
|
|
import { libraryStub } from 'test/fixtures/library.stub';
|
|
|
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
|
|
|
import { userStub } from 'test/fixtures/user.stub';
|
|
|
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
|
|
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
|
|
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
|
|
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
|
|
|
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
|
|
|
import { makeMockWatcher, newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
|
|
|
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
describe(LibraryService.name, () => {
|
|
|
|
let sut: LibraryService;
|
|
|
|
|
|
|
|
let assetMock: jest.Mocked<IAssetRepository>;
|
2023-10-31 21:19:12 +01:00
|
|
|
let configMock: jest.Mocked<ISystemConfigRepository>;
|
2023-09-20 13:16:33 +02:00
|
|
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
|
|
|
let jobMock: jest.Mocked<IJobRepository>;
|
|
|
|
let libraryMock: jest.Mocked<ILibraryRepository>;
|
|
|
|
let storageMock: jest.Mocked<IStorageRepository>;
|
2024-03-07 18:36:53 +01:00
|
|
|
let databaseMock: jest.Mocked<IDatabaseRepository>;
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
beforeEach(() => {
|
2023-10-31 21:19:12 +01:00
|
|
|
configMock = newSystemConfigRepositoryMock();
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock = newLibraryRepositoryMock();
|
|
|
|
assetMock = newAssetRepositoryMock();
|
|
|
|
jobMock = newJobRepositoryMock();
|
|
|
|
cryptoMock = newCryptoRepositoryMock();
|
|
|
|
storageMock = newStorageRepositoryMock();
|
2024-03-07 18:36:53 +01:00
|
|
|
databaseMock = newDatabaseRepositoryMock();
|
2023-09-20 13:16:33 +02:00
|
|
|
|
2024-03-20 19:32:04 +01:00
|
|
|
sut = new LibraryService(assetMock, configMock, cryptoMock, jobMock, libraryMock, storageMock, databaseMock);
|
2024-03-07 18:36:53 +01:00
|
|
|
|
|
|
|
databaseMock.tryLock.mockResolvedValue(true);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should work', () => {
|
|
|
|
expect(sut).toBeDefined();
|
|
|
|
});
|
|
|
|
|
2023-10-31 21:19:12 +01:00
|
|
|
describe('init', () => {
|
|
|
|
it('should init cron job and subscribe to config changes', async () => {
|
|
|
|
configMock.load.mockResolvedValue([
|
|
|
|
{ key: SystemConfigKey.LIBRARY_SCAN_ENABLED, value: true },
|
|
|
|
{ key: SystemConfigKey.LIBRARY_SCAN_CRON_EXPRESSION, value: '0 0 * * *' },
|
|
|
|
]);
|
|
|
|
|
|
|
|
await sut.init();
|
|
|
|
expect(configMock.load).toHaveBeenCalled();
|
|
|
|
expect(jobMock.addCronJob).toHaveBeenCalled();
|
|
|
|
|
|
|
|
SystemConfigCore.create(newSystemConfigRepositoryMock(false)).config$.next({
|
|
|
|
library: {
|
|
|
|
scan: {
|
|
|
|
enabled: true,
|
|
|
|
cronExpression: '0 1 * * *',
|
|
|
|
},
|
2024-01-31 09:15:54 +01:00
|
|
|
watch: { enabled: false },
|
2023-10-31 21:19:12 +01:00
|
|
|
},
|
|
|
|
} as SystemConfig);
|
|
|
|
|
|
|
|
expect(jobMock.updateCronJob).toHaveBeenCalledWith('libraryScan', '0 1 * * *', true);
|
|
|
|
});
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
it('should initialize watcher for all external libraries', async () => {
|
|
|
|
libraryMock.getAll.mockResolvedValue([
|
|
|
|
libraryStub.externalLibraryWithImportPaths1,
|
|
|
|
libraryStub.externalLibraryWithImportPaths2,
|
|
|
|
]);
|
|
|
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
|
2024-03-05 23:23:06 +01:00
|
|
|
when(libraryMock.get)
|
|
|
|
.calledWith(libraryStub.externalLibraryWithImportPaths1.id)
|
|
|
|
.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
|
|
|
|
when(libraryMock.get)
|
|
|
|
.calledWith(libraryStub.externalLibraryWithImportPaths2.id)
|
|
|
|
.mockResolvedValue(libraryStub.externalLibraryWithImportPaths2);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.init();
|
|
|
|
|
|
|
|
expect(storageMock.watch.mock.calls).toEqual(
|
|
|
|
expect.arrayContaining([
|
|
|
|
(libraryStub.externalLibrary1.importPaths, expect.anything()),
|
|
|
|
(libraryStub.externalLibrary2.importPaths, expect.anything()),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-03-07 18:36:53 +01:00
|
|
|
it('should not initialize watcher when watching is disabled', async () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
|
|
|
|
|
|
|
await sut.init();
|
|
|
|
|
|
|
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
|
|
|
});
|
2024-03-07 18:36:53 +01:00
|
|
|
|
|
|
|
it('should not initialize watcher when lock is taken', async () => {
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
|
|
databaseMock.tryLock.mockResolvedValue(false);
|
|
|
|
|
|
|
|
await sut.init();
|
|
|
|
|
|
|
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
|
|
|
});
|
2023-10-31 21:19:12 +01:00
|
|
|
});
|
|
|
|
|
2024-03-17 20:16:02 +01:00
|
|
|
describe('validateConfig', () => {
|
|
|
|
it('should allow a valid cron expression', () => {
|
|
|
|
expect(() =>
|
|
|
|
sut.validateConfig({
|
|
|
|
newConfig: { library: { scan: { cronExpression: '0 0 * * *' } } } as SystemConfig,
|
|
|
|
oldConfig: {} as SystemConfig,
|
|
|
|
}),
|
|
|
|
).not.toThrow(expect.stringContaining('Invalid cron expression'));
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should fail for an invalid cron expression', () => {
|
|
|
|
expect(() =>
|
|
|
|
sut.validateConfig({
|
|
|
|
newConfig: { library: { scan: { cronExpression: 'foo' } } } as SystemConfig,
|
|
|
|
oldConfig: {} as SystemConfig,
|
|
|
|
}),
|
|
|
|
).toThrow(/Invalid cron expression.*/);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
describe('handleQueueAssetRefresh', () => {
|
|
|
|
it('should queue new assets', async () => {
|
|
|
|
const mockLibraryJob: ILibraryRefreshJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
2024-03-14 06:52:30 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
|
|
storageMock.walk.mockImplementation(async function* generator() {
|
|
|
|
yield '/data/user1/photo.jpg';
|
|
|
|
});
|
2024-03-11 03:30:57 +01:00
|
|
|
assetMock.getLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
|
|
|
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN_ASSET,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: libraryStub.externalLibrary1.owner.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
},
|
2024-01-01 21:45:42 +01:00
|
|
|
},
|
2023-09-20 13:16:33 +02:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should force queue new assets', async () => {
|
|
|
|
const mockLibraryJob: ILibraryRefreshJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
2024-03-14 06:52:30 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
|
|
storageMock.walk.mockImplementation(async function* generator() {
|
|
|
|
yield '/data/user1/photo.jpg';
|
|
|
|
});
|
2024-03-11 03:30:57 +01:00
|
|
|
assetMock.getLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
|
|
|
|
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN_ASSET,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: libraryStub.externalLibrary1.owner.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
|
|
|
force: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
it('should not scan upload libraries', async () => {
|
|
|
|
const mockLibraryJob: ILibraryRefreshJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
2024-02-20 16:53:12 +01:00
|
|
|
|
|
|
|
it('should ignore import paths that do not exist', async () => {
|
|
|
|
storageMock.stat.mockImplementation((path): Promise<Stats> => {
|
|
|
|
if (path === libraryStub.externalLibraryWithImportPaths1.importPaths[0]) {
|
|
|
|
const error = { code: 'ENOENT' } as any;
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
return Promise.resolve({
|
|
|
|
isDirectory: () => true,
|
|
|
|
} as Stats);
|
|
|
|
});
|
|
|
|
|
|
|
|
storageMock.checkFileExists.mockResolvedValue(true);
|
|
|
|
|
|
|
|
const mockLibraryJob: ILibraryRefreshJob = {
|
|
|
|
id: libraryStub.externalLibraryWithImportPaths1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
2024-03-11 03:30:57 +01:00
|
|
|
assetMock.getLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
|
2024-02-20 16:53:12 +01:00
|
|
|
|
|
|
|
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
|
|
|
|
2024-03-14 06:52:30 +01:00
|
|
|
expect(storageMock.walk).toHaveBeenCalledWith({
|
2024-02-20 16:53:12 +01:00
|
|
|
pathsToCrawl: [libraryStub.externalLibraryWithImportPaths1.importPaths[1]],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
});
|
|
|
|
});
|
2024-03-11 03:30:57 +01:00
|
|
|
|
|
|
|
it('should set missing assets offline', async () => {
|
|
|
|
const mockLibraryJob: ILibraryRefreshJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
assetMock.getLibraryAssetPaths.mockResolvedValue({
|
|
|
|
items: [assetStub.image],
|
|
|
|
hasNextPage: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
|
|
|
|
|
|
|
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], { isOffline: true });
|
|
|
|
expect(assetMock.updateAll).not.toHaveBeenCalledWith(expect.anything(), { isOffline: false });
|
|
|
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should set crawled assets that were previously offline back online', async () => {
|
|
|
|
const mockLibraryJob: ILibraryRefreshJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
2024-03-14 06:52:30 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
|
|
storageMock.walk.mockImplementation(async function* generator() {
|
|
|
|
yield assetStub.offline.originalPath;
|
|
|
|
});
|
2024-03-11 03:30:57 +01:00
|
|
|
assetMock.getLibraryAssetPaths.mockResolvedValue({
|
|
|
|
items: [assetStub.offline],
|
|
|
|
hasNextPage: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
await sut.handleQueueAssetRefresh(mockLibraryJob);
|
|
|
|
|
|
|
|
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.offline.id], { isOffline: false });
|
|
|
|
expect(assetMock.updateAll).not.toHaveBeenCalledWith(expect.anything(), { isOffline: true });
|
|
|
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
|
|
|
});
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleAssetRefresh', () => {
|
|
|
|
let mockUser: UserEntity;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2024-02-29 19:35:37 +01:00
|
|
|
mockUser = userStub.admin;
|
2024-02-20 16:53:12 +01:00
|
|
|
|
|
|
|
storageMock.stat.mockResolvedValue({
|
|
|
|
size: 100,
|
|
|
|
mtime: new Date('2023-01-01'),
|
|
|
|
ctime: new Date('2023-01-01'),
|
|
|
|
} as Stats);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should reject an unknown file extension', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/file.xyz',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
|
|
|
|
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should reject an unknown file type', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/file.xyz',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should add a new image', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(assetMock.create.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
libraryId: libraryStub.externalLibrary1.id,
|
|
|
|
checksum: expect.any(Buffer),
|
|
|
|
originalPath: '/data/user1/photo.jpg',
|
|
|
|
deviceAssetId: expect.any(String),
|
|
|
|
deviceId: 'Library Import',
|
|
|
|
fileCreatedAt: expect.any(Date),
|
|
|
|
fileModifiedAt: expect.any(Date),
|
2023-10-05 00:11:11 +02:00
|
|
|
localDateTime: expect.any(Date),
|
2023-09-20 13:16:33 +02:00
|
|
|
type: AssetType.IMAGE,
|
2024-03-20 05:40:28 +01:00
|
|
|
originalFileName: 'photo.jpg',
|
2023-09-20 13:16:33 +02:00
|
|
|
sidecarPath: null,
|
|
|
|
isReadOnly: true,
|
|
|
|
isExternal: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
|
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
|
|
|
data: {
|
|
|
|
id: assetStub.image.id,
|
|
|
|
source: 'upload',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should add a new image with sidecar', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
storageMock.checkFileExists.mockResolvedValue(true);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(assetMock.create.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
libraryId: libraryStub.externalLibrary1.id,
|
|
|
|
checksum: expect.any(Buffer),
|
|
|
|
originalPath: '/data/user1/photo.jpg',
|
|
|
|
deviceAssetId: expect.any(String),
|
|
|
|
deviceId: 'Library Import',
|
|
|
|
fileCreatedAt: expect.any(Date),
|
|
|
|
fileModifiedAt: expect.any(Date),
|
2023-10-05 00:11:11 +02:00
|
|
|
localDateTime: expect.any(Date),
|
2023-09-20 13:16:33 +02:00
|
|
|
type: AssetType.IMAGE,
|
2024-03-20 05:40:28 +01:00
|
|
|
originalFileName: 'photo.jpg',
|
2023-09-20 13:16:33 +02:00
|
|
|
sidecarPath: '/data/user1/photo.jpg.xmp',
|
|
|
|
isReadOnly: true,
|
|
|
|
isExternal: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
|
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
|
|
|
data: {
|
|
|
|
id: assetStub.image.id,
|
|
|
|
source: 'upload',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should add a new video', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/video.mp4',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.video);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(assetMock.create.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
libraryId: libraryStub.externalLibrary1.id,
|
|
|
|
checksum: expect.any(Buffer),
|
|
|
|
originalPath: '/data/user1/video.mp4',
|
|
|
|
deviceAssetId: expect.any(String),
|
|
|
|
deviceId: 'Library Import',
|
|
|
|
fileCreatedAt: expect.any(Date),
|
|
|
|
fileModifiedAt: expect.any(Date),
|
2023-10-05 00:11:11 +02:00
|
|
|
localDateTime: expect.any(Date),
|
2023-09-20 13:16:33 +02:00
|
|
|
type: AssetType.VIDEO,
|
2024-03-20 05:40:28 +01:00
|
|
|
originalFileName: 'video.mp4',
|
2023-09-20 13:16:33 +02:00
|
|
|
sidecarPath: null,
|
|
|
|
isReadOnly: true,
|
|
|
|
isExternal: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
|
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
|
|
|
data: {
|
|
|
|
id: assetStub.image.id,
|
|
|
|
source: 'upload',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.VIDEO_CONVERSION,
|
|
|
|
data: {
|
|
|
|
id: assetStub.video.id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not add an image to a soft deleted library', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() });
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(assetMock.create.mock.calls).toEqual([]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not import an asset when mtime matches db asset', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
storageMock.stat.mockResolvedValue({
|
|
|
|
size: 100,
|
|
|
|
mtime: assetStub.image.fileModifiedAt,
|
|
|
|
ctime: new Date('2023-01-01'),
|
|
|
|
} as Stats);
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should import an asset when mtime differs from db asset', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
|
|
|
data: {
|
|
|
|
id: assetStub.image.id,
|
|
|
|
source: 'upload',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(jobMock.queue).not.toHaveBeenCalledWith({
|
|
|
|
name: JobName.VIDEO_CONVERSION,
|
|
|
|
data: {
|
|
|
|
id: assetStub.image.id,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should set a missing asset to offline', async () => {
|
2024-02-02 04:18:00 +01:00
|
|
|
storageMock.stat.mockRejectedValue(new Error('Path not found'));
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: assetStub.image.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true });
|
2023-09-20 13:16:33 +02:00
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should online a previously-offline asset', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: assetStub.offline.id,
|
|
|
|
ownerId: mockUser.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.offline);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.offline);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false });
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
|
|
|
data: {
|
|
|
|
id: assetStub.offline.id,
|
|
|
|
source: 'upload',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(jobMock.queue).not.toHaveBeenCalledWith({
|
|
|
|
name: JobName.VIDEO_CONVERSION,
|
|
|
|
data: {
|
|
|
|
id: assetStub.offline.id,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should do nothing when mtime matches existing asset', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: assetStub.image.id,
|
|
|
|
ownerId: assetStub.image.ownerId,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2023-09-20 13:16:33 +02:00
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should refresh an existing asset if forced', async () => {
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: assetStub.image.id,
|
|
|
|
ownerId: assetStub.image.ownerId,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: true,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], {
|
|
|
|
fileCreatedAt: new Date('2023-01-01'),
|
|
|
|
fileModifiedAt: new Date('2023-01-01'),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should refresh an existing asset with modified mtime', async () => {
|
|
|
|
const filemtime = new Date();
|
|
|
|
filemtime.setSeconds(assetStub.image.fileModifiedAt.getSeconds() + 10);
|
|
|
|
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: userStub.admin.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
storageMock.stat.mockResolvedValue({
|
|
|
|
size: 100,
|
|
|
|
mtime: filemtime,
|
|
|
|
ctime: new Date('2023-01-01'),
|
|
|
|
} as Stats);
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(assetMock.create).toHaveBeenCalled();
|
|
|
|
const createdAsset = assetMock.create.mock.calls[0][0];
|
|
|
|
|
|
|
|
expect(createdAsset.fileModifiedAt).toEqual(filemtime);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should throw error when asset does not exist', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
storageMock.stat.mockRejectedValue(new Error("ENOENT, no such file or directory '/data/user1/photo.jpg'"));
|
|
|
|
|
|
|
|
const mockLibraryJob: ILibraryFileJob = {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
ownerId: userStub.admin.id,
|
|
|
|
assetPath: '/data/user1/photo.jpg',
|
2023-10-09 05:16:13 +02:00
|
|
|
force: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.image);
|
|
|
|
|
|
|
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('delete', () => {
|
|
|
|
it('should delete a library', async () => {
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
libraryMock.getUploadLibraryCount.mockResolvedValue(2);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await sut.delete(libraryStub.externalLibrary1.id);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
|
|
|
name: JobName.LIBRARY_DELETE,
|
|
|
|
data: { id: libraryStub.externalLibrary1.id },
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(libraryMock.softDelete).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw error if the last upload library is deleted', async () => {
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.delete(libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf(BadRequestException);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
2023-09-20 13:16:33 +02:00
|
|
|
expect(libraryMock.softDelete).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should allow an external library to be deleted', async () => {
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await sut.delete(libraryStub.externalLibrary1.id);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
|
|
|
name: JobName.LIBRARY_DELETE,
|
|
|
|
data: { id: libraryStub.externalLibrary1.id },
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(libraryMock.softDelete).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
|
|
|
|
});
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
it('should unwatch an external library when deleted', async () => {
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
|
|
|
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
|
|
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
|
|
|
2024-02-13 14:48:47 +01:00
|
|
|
const mockClose = jest.fn();
|
|
|
|
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.init();
|
2024-03-18 21:59:53 +01:00
|
|
|
await sut.delete(libraryStub.externalLibraryWithImportPaths1.id);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
2024-02-13 14:48:47 +01:00
|
|
|
expect(mockClose).toHaveBeenCalled();
|
2024-01-31 09:15:54 +01:00
|
|
|
});
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('get', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should return a library', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.get(libraryStub.uploadLibrary1.id)).resolves.toEqual(
|
2023-09-20 13:16:33 +02:00
|
|
|
expect.objectContaining({
|
|
|
|
id: libraryStub.uploadLibrary1.id,
|
|
|
|
name: libraryStub.uploadLibrary1.name,
|
|
|
|
ownerId: libraryStub.uploadLibrary1.ownerId,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.uploadLibrary1.id);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw an error when a library is not found', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(null);
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.get(libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf(BadRequestException);
|
2023-09-20 13:16:33 +02:00
|
|
|
expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.uploadLibrary1.id);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('getStatistics', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should return library statistics', async () => {
|
2024-03-18 21:59:53 +01:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.getStatistics(libraryStub.uploadLibrary1.id)).resolves.toEqual({
|
2023-09-20 13:16:33 +02:00
|
|
|
photos: 10,
|
|
|
|
videos: 0,
|
|
|
|
total: 10,
|
|
|
|
usage: 1337,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(libraryMock.getStatistics).toHaveBeenCalledWith(libraryStub.uploadLibrary1.id);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('create', () => {
|
|
|
|
describe('external library', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should create with default settings', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.EXTERNAL })).resolves.toEqual(
|
2023-09-20 13:16:33 +02:00
|
|
|
expect.objectContaining({
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
name: libraryStub.externalLibrary1.name,
|
|
|
|
ownerId: libraryStub.externalLibrary1.ownerId,
|
|
|
|
assetCount: 0,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
createdAt: libraryStub.externalLibrary1.createdAt,
|
|
|
|
updatedAt: libraryStub.externalLibrary1.updatedAt,
|
|
|
|
refreshedAt: null,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(libraryMock.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
2024-01-31 09:15:54 +01:00
|
|
|
name: expect.any(String),
|
2023-09-20 13:16:33 +02:00
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
isVisible: true,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should create with name', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
await expect(
|
2024-03-18 21:59:53 +01:00
|
|
|
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.EXTERNAL, name: 'My Awesome Library' }),
|
2023-09-20 13:16:33 +02:00
|
|
|
).resolves.toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
name: libraryStub.externalLibrary1.name,
|
|
|
|
ownerId: libraryStub.externalLibrary1.ownerId,
|
|
|
|
assetCount: 0,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
createdAt: libraryStub.externalLibrary1.createdAt,
|
|
|
|
updatedAt: libraryStub.externalLibrary1.updatedAt,
|
|
|
|
refreshedAt: null,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(libraryMock.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
name: 'My Awesome Library',
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
isVisible: true,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should create invisible', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
await expect(
|
2024-03-18 21:59:53 +01:00
|
|
|
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.EXTERNAL, isVisible: false }),
|
2023-09-20 13:16:33 +02:00
|
|
|
).resolves.toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
name: libraryStub.externalLibrary1.name,
|
|
|
|
ownerId: libraryStub.externalLibrary1.ownerId,
|
|
|
|
assetCount: 0,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
createdAt: libraryStub.externalLibrary1.createdAt,
|
|
|
|
updatedAt: libraryStub.externalLibrary1.updatedAt,
|
|
|
|
refreshedAt: null,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(libraryMock.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
2024-01-31 09:15:54 +01:00
|
|
|
name: expect.any(String),
|
2023-09-20 13:16:33 +02:00
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
isVisible: false,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should create with import paths', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
await expect(
|
2024-03-18 21:59:53 +01:00
|
|
|
sut.create({
|
|
|
|
ownerId: authStub.admin.user.id,
|
2023-09-20 13:16:33 +02:00
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: ['/data/images', '/data/videos'],
|
|
|
|
}),
|
|
|
|
).resolves.toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
name: libraryStub.externalLibrary1.name,
|
|
|
|
ownerId: libraryStub.externalLibrary1.ownerId,
|
|
|
|
assetCount: 0,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
createdAt: libraryStub.externalLibrary1.createdAt,
|
|
|
|
updatedAt: libraryStub.externalLibrary1.updatedAt,
|
|
|
|
refreshedAt: null,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(libraryMock.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
2024-01-31 09:15:54 +01:00
|
|
|
name: expect.any(String),
|
2023-09-20 13:16:33 +02:00
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: ['/data/images', '/data/videos'],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
isVisible: true,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should create watched with import paths', async () => {
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
|
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([]);
|
|
|
|
|
|
|
|
await sut.init();
|
2024-03-18 21:59:53 +01:00
|
|
|
await sut.create({
|
|
|
|
ownerId: authStub.admin.user.id,
|
2024-01-31 09:15:54 +01:00
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(storageMock.watch).toHaveBeenCalledWith(
|
|
|
|
libraryStub.externalLibraryWithImportPaths1.importPaths,
|
|
|
|
expect.anything(),
|
2024-02-13 14:48:47 +01:00
|
|
|
expect.anything(),
|
2024-01-31 09:15:54 +01:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should create with exclusion patterns', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
await expect(
|
2024-03-18 21:59:53 +01:00
|
|
|
sut.create({
|
|
|
|
ownerId: authStub.admin.user.id,
|
2023-09-20 13:16:33 +02:00
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
exclusionPatterns: ['*.tmp', '*.bak'],
|
|
|
|
}),
|
|
|
|
).resolves.toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
name: libraryStub.externalLibrary1.name,
|
|
|
|
ownerId: libraryStub.externalLibrary1.ownerId,
|
|
|
|
assetCount: 0,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
createdAt: libraryStub.externalLibrary1.createdAt,
|
|
|
|
updatedAt: libraryStub.externalLibrary1.updatedAt,
|
|
|
|
refreshedAt: null,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(libraryMock.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
2024-01-31 09:15:54 +01:00
|
|
|
name: expect.any(String),
|
2023-09-20 13:16:33 +02:00
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: ['*.tmp', '*.bak'],
|
|
|
|
isVisible: true,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('upload library', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should create with default settings', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.create.mockResolvedValue(libraryStub.uploadLibrary1);
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD })).resolves.toEqual(
|
2023-09-20 13:16:33 +02:00
|
|
|
expect.objectContaining({
|
|
|
|
id: libraryStub.uploadLibrary1.id,
|
|
|
|
type: LibraryType.UPLOAD,
|
|
|
|
name: libraryStub.uploadLibrary1.name,
|
|
|
|
ownerId: libraryStub.uploadLibrary1.ownerId,
|
|
|
|
assetCount: 0,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
createdAt: libraryStub.uploadLibrary1.createdAt,
|
|
|
|
updatedAt: libraryStub.uploadLibrary1.updatedAt,
|
|
|
|
refreshedAt: null,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(libraryMock.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
name: 'New Upload Library',
|
|
|
|
type: LibraryType.UPLOAD,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
isVisible: true,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should create with name', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.create.mockResolvedValue(libraryStub.uploadLibrary1);
|
|
|
|
await expect(
|
2024-03-18 21:59:53 +01:00
|
|
|
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD, name: 'My Awesome Library' }),
|
2023-09-20 13:16:33 +02:00
|
|
|
).resolves.toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
id: libraryStub.uploadLibrary1.id,
|
|
|
|
type: LibraryType.UPLOAD,
|
|
|
|
name: libraryStub.uploadLibrary1.name,
|
|
|
|
ownerId: libraryStub.uploadLibrary1.ownerId,
|
|
|
|
assetCount: 0,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
createdAt: libraryStub.uploadLibrary1.createdAt,
|
|
|
|
updatedAt: libraryStub.uploadLibrary1.updatedAt,
|
|
|
|
refreshedAt: null,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(libraryMock.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
name: 'My Awesome Library',
|
|
|
|
type: LibraryType.UPLOAD,
|
|
|
|
importPaths: [],
|
|
|
|
exclusionPatterns: [],
|
|
|
|
isVisible: true,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should not create with import paths', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
await expect(
|
2024-03-18 21:59:53 +01:00
|
|
|
sut.create({
|
|
|
|
ownerId: authStub.admin.user.id,
|
2023-09-20 13:16:33 +02:00
|
|
|
type: LibraryType.UPLOAD,
|
|
|
|
importPaths: ['/data/images', '/data/videos'],
|
|
|
|
}),
|
|
|
|
).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
|
|
|
|
expect(libraryMock.create).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should not create with exclusion patterns', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
await expect(
|
2024-03-18 21:59:53 +01:00
|
|
|
sut.create({
|
|
|
|
ownerId: authStub.admin.user.id,
|
2023-09-20 13:16:33 +02:00
|
|
|
type: LibraryType.UPLOAD,
|
|
|
|
exclusionPatterns: ['*.tmp', '*.bak'],
|
|
|
|
}),
|
|
|
|
).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
|
|
|
|
expect(libraryMock.create).not.toHaveBeenCalled();
|
|
|
|
});
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
it('should not create watched', async () => {
|
|
|
|
await expect(
|
2024-03-18 21:59:53 +01:00
|
|
|
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD, isWatched: true }),
|
2024-01-31 09:15:54 +01:00
|
|
|
).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
|
|
|
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
|
|
|
});
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleQueueCleanup', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should queue cleanup jobs', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.getAllDeleted.mockResolvedValue([libraryStub.uploadLibrary1, libraryStub.externalLibrary1]);
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleQueueCleanup()).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{ name: JobName.LIBRARY_DELETE, data: { id: libraryStub.uploadLibrary1.id } },
|
|
|
|
{ name: JobName.LIBRARY_DELETE, data: { id: libraryStub.externalLibrary1.id } },
|
2023-09-20 13:16:33 +02:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('update', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
beforeEach(async () => {
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
|
|
libraryMock.getAll.mockResolvedValue([]);
|
|
|
|
|
|
|
|
await sut.init();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should update library', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.update.mockResolvedValue(libraryStub.uploadLibrary1);
|
2024-03-18 21:59:53 +01:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
|
|
|
await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.uploadLibrary1));
|
|
|
|
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
2024-01-31 09:15:54 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should re-watch library when updating import paths', async () => {
|
|
|
|
libraryMock.update.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
|
2024-02-20 16:53:12 +01:00
|
|
|
storageMock.stat.mockResolvedValue({
|
|
|
|
isDirectory: () => true,
|
|
|
|
} as Stats);
|
|
|
|
|
|
|
|
storageMock.checkFileExists.mockResolvedValue(true);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.update('library-id', { importPaths: ['/data/user1/foo'] })).resolves.toEqual(
|
|
|
|
mapLibrary(libraryStub.externalLibraryWithImportPaths1),
|
2024-01-31 09:15:54 +01:00
|
|
|
);
|
2024-03-18 21:59:53 +01:00
|
|
|
|
|
|
|
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
2024-01-31 09:15:54 +01:00
|
|
|
expect(storageMock.watch).toHaveBeenCalledWith(
|
|
|
|
libraryStub.externalLibraryWithImportPaths1.importPaths,
|
|
|
|
expect.anything(),
|
2024-02-13 14:48:47 +01:00
|
|
|
expect.anything(),
|
2024-01-31 09:15:54 +01:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should re-watch library when updating exclusion patterns', async () => {
|
|
|
|
libraryMock.update.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.update('library-id', { exclusionPatterns: ['bar'] })).resolves.toEqual(
|
2024-01-31 09:15:54 +01:00
|
|
|
mapLibrary(libraryStub.externalLibraryWithImportPaths1),
|
|
|
|
);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
2024-02-13 14:48:47 +01:00
|
|
|
expect(storageMock.watch).toHaveBeenCalledWith(
|
|
|
|
expect.arrayContaining([expect.any(String)]),
|
|
|
|
expect.anything(),
|
|
|
|
expect.anything(),
|
|
|
|
);
|
2024-01-31 09:15:54 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-02-20 16:53:12 +01:00
|
|
|
describe('watchAll', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
describe('watching disabled', () => {
|
|
|
|
beforeEach(async () => {
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
|
|
|
|
|
|
|
await sut.init();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not watch library', async () => {
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
|
|
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('watching enabled', () => {
|
|
|
|
beforeEach(async () => {
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
|
|
libraryMock.getAll.mockResolvedValue([]);
|
|
|
|
await sut.init();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should watch library', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
|
|
|
expect(storageMock.watch).toHaveBeenCalledWith(
|
|
|
|
libraryStub.externalLibraryWithImportPaths1.importPaths,
|
|
|
|
expect.anything(),
|
2024-02-13 14:48:47 +01:00
|
|
|
expect.anything(),
|
2024-01-31 09:15:54 +01:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should watch and unwatch library', async () => {
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
2024-02-13 14:48:47 +01:00
|
|
|
const mockClose = jest.fn();
|
|
|
|
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
await sut.unwatch(libraryStub.externalLibraryWithImportPaths1.id);
|
|
|
|
|
2024-02-13 14:48:47 +01:00
|
|
|
expect(mockClose).toHaveBeenCalled();
|
2024-01-31 09:15:54 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should not watch library without import paths', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
|
|
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw error when watching upload library', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.uploadLibrary1]);
|
|
|
|
|
|
|
|
await expect(sut.watchAll()).rejects.toThrow('Can only watch external libraries');
|
|
|
|
|
|
|
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle a new file event', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
2024-03-07 18:36:53 +01:00
|
|
|
storageMock.watch.mockImplementation(
|
|
|
|
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }),
|
|
|
|
);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN_ASSET,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibraryWithImportPaths1.id,
|
|
|
|
assetPath: '/foo/photo.jpg',
|
|
|
|
ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id,
|
|
|
|
force: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle a file change event', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
2024-02-13 14:48:47 +01:00
|
|
|
storageMock.watch.mockImplementation(
|
2024-03-07 18:36:53 +01:00
|
|
|
makeMockWatcher({ items: [{ event: StorageEventType.CHANGE, value: '/foo/photo.jpg' }] }),
|
2024-02-13 14:48:47 +01:00
|
|
|
);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN_ASSET,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibraryWithImportPaths1.id,
|
|
|
|
assetPath: '/foo/photo.jpg',
|
|
|
|
ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id,
|
|
|
|
force: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle a file unlink event', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
|
2024-02-13 14:48:47 +01:00
|
|
|
storageMock.watch.mockImplementation(
|
2024-03-07 18:36:53 +01:00
|
|
|
makeMockWatcher({ items: [{ event: StorageEventType.UNLINK, value: '/foo/photo.jpg' }] }),
|
2024-02-13 14:48:47 +01:00
|
|
|
);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
|
2024-01-31 09:15:54 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle an error event', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
2024-02-13 14:48:47 +01:00
|
|
|
storageMock.watch.mockImplementation(
|
|
|
|
makeMockWatcher({
|
2024-03-07 18:36:53 +01:00
|
|
|
items: [{ event: StorageEventType.ERROR, value: 'Error!' }],
|
2024-02-13 14:48:47 +01:00
|
|
|
}),
|
|
|
|
);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
2024-03-07 18:36:53 +01:00
|
|
|
await expect(sut.watchAll()).rejects.toThrow('Error!');
|
2024-01-31 09:15:54 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should ignore unknown extensions', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
2024-03-07 18:36:53 +01:00
|
|
|
storageMock.watch.mockImplementation(
|
|
|
|
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }),
|
|
|
|
);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should ignore excluded paths', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.patternPath);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
|
2024-03-07 18:36:53 +01:00
|
|
|
storageMock.watch.mockImplementation(
|
|
|
|
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/dir1/photo.txt' }] }),
|
|
|
|
);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should ignore excluded paths without case sensitivity', async () => {
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.patternPath);
|
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
|
2024-03-07 18:36:53 +01:00
|
|
|
storageMock.watch.mockImplementation(
|
|
|
|
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/DIR1/photo.txt' }] }),
|
|
|
|
);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.watchAll();
|
|
|
|
|
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-03-07 18:36:53 +01:00
|
|
|
describe('teardown', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should tear down all watchers', async () => {
|
|
|
|
libraryMock.getAll.mockResolvedValue([
|
|
|
|
libraryStub.externalLibraryWithImportPaths1,
|
|
|
|
libraryStub.externalLibraryWithImportPaths2,
|
|
|
|
]);
|
|
|
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
|
2024-03-05 23:23:06 +01:00
|
|
|
when(libraryMock.get)
|
|
|
|
.calledWith(libraryStub.externalLibraryWithImportPaths1.id)
|
|
|
|
.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
|
|
|
|
|
|
|
when(libraryMock.get)
|
|
|
|
.calledWith(libraryStub.externalLibraryWithImportPaths2.id)
|
|
|
|
.mockResolvedValue(libraryStub.externalLibraryWithImportPaths2);
|
2024-01-31 09:15:54 +01:00
|
|
|
|
2024-02-13 14:48:47 +01:00
|
|
|
const mockClose = jest.fn();
|
|
|
|
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
2024-01-31 09:15:54 +01:00
|
|
|
|
|
|
|
await sut.init();
|
2024-03-07 18:36:53 +01:00
|
|
|
await sut.teardown();
|
2024-01-31 09:15:54 +01:00
|
|
|
|
2024-02-13 14:48:47 +01:00
|
|
|
expect(mockClose).toHaveBeenCalledTimes(2);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleDeleteLibrary', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should not delete a nonexistent library', async () => {
|
2024-03-05 23:23:06 +01:00
|
|
|
libraryMock.get.mockResolvedValue(null);
|
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.getAssetIds.mockResolvedValue([]);
|
|
|
|
libraryMock.delete.mockImplementation(async () => {});
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.FAILED);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should delete an empty library', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
|
|
|
libraryMock.getAssetIds.mockResolvedValue([]);
|
|
|
|
libraryMock.delete.mockImplementation(async () => {});
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should delete a library with assets', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
|
|
|
libraryMock.getAssetIds.mockResolvedValue([assetStub.image1.id]);
|
|
|
|
libraryMock.delete.mockImplementation(async () => {});
|
|
|
|
|
|
|
|
assetMock.getById.mockResolvedValue(assetStub.image1);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('queueScan', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should queue a library scan of external library', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await sut.queueScan(libraryStub.externalLibrary1.id, {});
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should not queue a library scan of upload library', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.queueScan(libraryStub.uploadLibrary1.id, {})).rejects.toBeInstanceOf(BadRequestException);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue).not.toBeCalled();
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should queue a library scan of all modified assets', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await sut.queueScan(libraryStub.externalLibrary1.id, { refreshModifiedFiles: true });
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: true,
|
|
|
|
refreshAllFiles: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should queue a forced library scan', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await sut.queueScan(libraryStub.externalLibrary1.id, { refreshAllFiles: true });
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('queueEmptyTrash', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should queue the trash job', async () => {
|
2024-03-18 21:59:53 +01:00
|
|
|
await sut.queueRemoveOffline(libraryStub.externalLibrary1.id);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_REMOVE_OFFLINE,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleQueueAllScan', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should queue the refresh job', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleQueueAllScan({})).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_QUEUE_CLEANUP,
|
|
|
|
data: {},
|
|
|
|
},
|
|
|
|
],
|
2024-01-01 21:45:42 +01:00
|
|
|
]);
|
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: true,
|
|
|
|
refreshAllFiles: false,
|
2023-09-20 13:16:33 +02:00
|
|
|
},
|
2024-01-01 21:45:42 +01:00
|
|
|
},
|
2023-09-20 13:16:33 +02:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should queue the force refresh job', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleQueueAllScan({ force: true })).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
2024-01-31 09:15:54 +01:00
|
|
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
|
|
|
name: JobName.LIBRARY_QUEUE_CLEANUP,
|
|
|
|
data: {},
|
|
|
|
});
|
|
|
|
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.LIBRARY_SCAN,
|
|
|
|
data: {
|
|
|
|
id: libraryStub.externalLibrary1.id,
|
|
|
|
refreshModifiedFiles: false,
|
|
|
|
refreshAllFiles: true,
|
2023-09-20 13:16:33 +02:00
|
|
|
},
|
2024-01-01 21:45:42 +01:00
|
|
|
},
|
2023-09-20 13:16:33 +02:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-10-06 23:32:28 +02:00
|
|
|
describe('handleRemoveOfflineFiles', () => {
|
2024-01-31 09:15:54 +01:00
|
|
|
it('should queue trash deletion jobs', async () => {
|
2023-09-20 13:16:33 +02:00
|
|
|
assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
|
|
|
|
assetMock.getById.mockResolvedValue(assetStub.image1);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.ASSET_DELETION,
|
|
|
|
data: { id: assetStub.image1.id, fromExternal: true },
|
|
|
|
},
|
2023-09-20 13:16:33 +02:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
2024-02-20 16:53:12 +01:00
|
|
|
|
|
|
|
describe('validate', () => {
|
|
|
|
it('should validate directory', async () => {
|
|
|
|
storageMock.stat.mockResolvedValue({
|
|
|
|
isDirectory: () => true,
|
|
|
|
} as Stats);
|
|
|
|
|
|
|
|
storageMock.checkFileExists.mockResolvedValue(true);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({
|
|
|
|
importPaths: [
|
|
|
|
{
|
|
|
|
importPath: '/data/user1/',
|
|
|
|
isValid: true,
|
|
|
|
message: undefined,
|
|
|
|
},
|
|
|
|
],
|
2024-02-20 16:53:12 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should detect when path does not exist', async () => {
|
|
|
|
storageMock.stat.mockImplementation(() => {
|
|
|
|
const error = { code: 'ENOENT' } as any;
|
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({
|
|
|
|
importPaths: [
|
|
|
|
{
|
|
|
|
importPath: '/data/user1/',
|
|
|
|
isValid: false,
|
|
|
|
message: 'Path does not exist (ENOENT)',
|
|
|
|
},
|
|
|
|
],
|
2024-02-20 16:53:12 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should detect when path is not a directory', async () => {
|
|
|
|
storageMock.stat.mockResolvedValue({
|
|
|
|
isDirectory: () => false,
|
|
|
|
} as Stats);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/file'] })).resolves.toEqual({
|
|
|
|
importPaths: [
|
|
|
|
{
|
|
|
|
importPath: '/data/user1/file',
|
|
|
|
isValid: false,
|
|
|
|
message: 'Not a directory',
|
|
|
|
},
|
|
|
|
],
|
2024-02-20 16:53:12 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should return an unknown exception from stat', async () => {
|
|
|
|
storageMock.stat.mockImplementation(() => {
|
|
|
|
throw new Error('Unknown error');
|
|
|
|
});
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({
|
|
|
|
importPaths: [
|
|
|
|
{
|
|
|
|
importPath: '/data/user1/',
|
|
|
|
isValid: false,
|
|
|
|
message: 'Error: Unknown error',
|
|
|
|
},
|
|
|
|
],
|
2024-02-20 16:53:12 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should detect when access rights are missing', async () => {
|
|
|
|
storageMock.stat.mockResolvedValue({
|
|
|
|
isDirectory: () => true,
|
|
|
|
} as Stats);
|
|
|
|
|
|
|
|
storageMock.checkFileExists.mockResolvedValue(false);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(sut.validate('library-id', { importPaths: ['/data/user1/'] })).resolves.toEqual({
|
|
|
|
importPaths: [
|
|
|
|
{
|
|
|
|
importPath: '/data/user1/',
|
|
|
|
isValid: false,
|
|
|
|
message: 'Lacking read permission for folder',
|
|
|
|
},
|
|
|
|
],
|
2024-02-20 16:53:12 +01:00
|
|
|
});
|
|
|
|
});
|
2024-03-15 23:01:58 +01:00
|
|
|
|
|
|
|
it('should detect when import path is in immich media folder', async () => {
|
|
|
|
storageMock.stat.mockResolvedValue({ isDirectory: () => true } as Stats);
|
|
|
|
const validImport = libraryStub.hasImmichPaths.importPaths[1];
|
|
|
|
when(storageMock.checkFileExists).calledWith(validImport, R_OK).mockResolvedValue(true);
|
|
|
|
|
2024-03-18 21:59:53 +01:00
|
|
|
await expect(
|
|
|
|
sut.validate('library-id', { importPaths: libraryStub.hasImmichPaths.importPaths }),
|
|
|
|
).resolves.toEqual({
|
|
|
|
importPaths: [
|
|
|
|
{
|
|
|
|
importPath: libraryStub.hasImmichPaths.importPaths[0],
|
|
|
|
isValid: false,
|
|
|
|
message: 'Cannot use media upload folder for external libraries',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
importPath: validImport,
|
|
|
|
isValid: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
importPath: libraryStub.hasImmichPaths.importPaths[2],
|
|
|
|
isValid: false,
|
|
|
|
message: 'Cannot use media upload folder for external libraries',
|
|
|
|
},
|
|
|
|
],
|
2024-03-15 23:01:58 +01:00
|
|
|
});
|
|
|
|
});
|
2024-02-20 16:53:12 +01:00
|
|
|
});
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|