mirror of
https://github.com/immich-app/immich.git
synced 2025-04-21 07:26:25 +02:00
refactor: test mocks (#16008)
This commit is contained in:
parent
8794c84e9d
commit
735f8d661e
74 changed files with 3820 additions and 4043 deletions
server
src
cores
repositories
asset.repository.tslogging.repository.spec.tsnotification.repository.spec.tsstorage.repository.spec.ts
services
activity.service.spec.tsalbum.service.spec.tsapi-key.service.spec.tsasset-media.service.spec.tsasset.service.spec.tsaudit.service.spec.tsauth.service.spec.tsbackup.service.spec.tsbase.service.tscli.service.spec.tsdatabase.service.spec.tsdownload.service.spec.tsduplicate.service.spec.tsjob.service.spec.tslibrary.service.spec.tsmap.service.spec.tsmedia.service.spec.tsmemory.service.spec.tsmetadata.service.spec.tsnotification.service.spec.tspartner.service.spec.tsperson.service.spec.tssearch.service.spec.tsserver.service.spec.tssession.service.spec.tsshared-link.service.spec.tssmart-info.service.spec.tsstack.service.spec.tsstorage-template.service.spec.tsstorage.service.spec.tssync.service.spec.tssystem-config.service.spec.tssystem-metadata.service.spec.tstag.service.spec.tstimeline.service.spec.tstrash.service.spec.tsuser-admin.service.spec.tsuser.service.spec.tsversion.service.spec.tsview.service.spec.ts
types.tsutils
test
medium
repositories
access.repository.mock.tsactivity.repository.mock.tsalbum-user.repository.mock.tsapi-key.repository.mock.tsaudit.repository.mock.tsconfig.repository.mock.tscron.repository.mock.tslogger.repository.mock.tsmap.repository.mock.tsmedia.repository.mock.tsmemory.repository.mock.tsmetadata.repository.mock.tsnotification.repository.mock.tsoauth.repository.mock.tsprocess.repository.mock.tsserver-info.repository.mock.tssession.repository.mock.tssystem-metadata.repository.mock.tstelemetry.repository.mock.tstrash.repository.mock.tsversion-history.repository.mock.tsview.repository.mock.ts
utils.ts
|
@ -5,11 +5,13 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { getConfig } from 'src/utils/config';
|
||||
|
||||
|
@ -32,24 +34,24 @@ let instance: StorageCore | null;
|
|||
export class StorageCore {
|
||||
private constructor(
|
||||
private assetRepository: IAssetRepository,
|
||||
private configRepository: IConfigRepository,
|
||||
private cryptoRepository: ICryptoRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
private cryptoRepository: CryptoRepository,
|
||||
private moveRepository: IMoveRepository,
|
||||
private personRepository: IPersonRepository,
|
||||
private storageRepository: IStorageRepository,
|
||||
private systemMetadataRepository: ISystemMetadataRepository,
|
||||
private logger: ILoggingRepository,
|
||||
private systemMetadataRepository: SystemMetadataRepository,
|
||||
private logger: LoggingRepository,
|
||||
) {}
|
||||
|
||||
static create(
|
||||
assetRepository: IAssetRepository,
|
||||
configRepository: IConfigRepository,
|
||||
cryptoRepository: ICryptoRepository,
|
||||
configRepository: ConfigRepository,
|
||||
cryptoRepository: CryptoRepository,
|
||||
moveRepository: IMoveRepository,
|
||||
personRepository: IPersonRepository,
|
||||
storageRepository: IStorageRepository,
|
||||
systemMetadataRepository: ISystemMetadataRepository,
|
||||
logger: ILoggingRepository,
|
||||
systemMetadataRepository: SystemMetadataRepository,
|
||||
logger: LoggingRepository,
|
||||
) {
|
||||
if (!instance) {
|
||||
instance = new StorageCore(
|
||||
|
|
|
@ -524,7 +524,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
.executeTakeFirst() as Promise<AssetEntity | undefined>;
|
||||
}
|
||||
|
||||
getMapMarkers(ownerIds: string[], options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
|
||||
private getMapMarkers(ownerIds: string[], options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
|
||||
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
|
||||
|
||||
return this.db
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { ClsService } from 'nestjs-cls';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
describe(LoggingRepository.name, () => {
|
||||
let sut: LoggingRepository;
|
||||
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let configMock: Mocked<ConfigRepository>;
|
||||
let clsMock: Mocked<ClsService>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { ILoggingRepository } from 'src/types';
|
||||
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
describe(NotificationRepository.name, () => {
|
||||
let sut: NotificationRepository;
|
||||
let loggerMock: Mocked<ILoggingRepository>;
|
||||
let loggerMock: Mocked<LoggingRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
loggerMock = newLoggingRepositoryMock();
|
||||
loggerMock = newLoggingRepositoryMock() as ILoggingRepository as Mocked<LoggingRepository>;
|
||||
|
||||
sut = new NotificationRepository(loggerMock as ILoggingRepository as LoggingRepository);
|
||||
sut = new NotificationRepository(loggerMock as LoggingRepository);
|
||||
});
|
||||
|
||||
describe('renderEmail', () => {
|
||||
|
|
|
@ -2,8 +2,8 @@ import mockfs from 'mock-fs';
|
|||
import { CrawlOptionsDto } from 'src/dtos/library.dto';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { ILoggingRepository } from 'src/types';
|
||||
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
interface Test {
|
||||
test: string;
|
||||
|
@ -182,11 +182,11 @@ const tests: Test[] = [
|
|||
|
||||
describe(StorageRepository.name, () => {
|
||||
let sut: StorageRepository;
|
||||
let logger: ILoggingRepository;
|
||||
let logger: Mocked<ILoggingRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = newLoggingRepositoryMock();
|
||||
sut = new StorageRepository(logger as LoggingRepository);
|
||||
sut = new StorageRepository(logger as ILoggingRepository as LoggingRepository);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { ReactionType } from 'src/dtos/activity.dto';
|
||||
import { ActivityService } from 'src/services/activity.service';
|
||||
import { IActivityRepository } from 'src/types';
|
||||
import { activityStub } from 'test/fixtures/activity.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(ActivityService.name, () => {
|
||||
let sut: ActivityService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let activityMock: Mocked<IActivityRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, activityMock } = newTestService(ActivityService));
|
||||
({ sut, mocks } = newTestService(ActivityService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -24,12 +19,12 @@ describe(ActivityService.name, () => {
|
|||
|
||||
describe('getAll', () => {
|
||||
it('should get all', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.activity.search.mockResolvedValue([]);
|
||||
|
||||
await expect(sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id' })).resolves.toEqual([]);
|
||||
|
||||
expect(activityMock.search).toHaveBeenCalledWith({
|
||||
expect(mocks.activity.search).toHaveBeenCalledWith({
|
||||
assetId: 'asset-id',
|
||||
albumId: 'album-id',
|
||||
isLiked: undefined,
|
||||
|
@ -37,14 +32,14 @@ describe(ActivityService.name, () => {
|
|||
});
|
||||
|
||||
it('should filter by type=like', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.activity.search.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id', type: ReactionType.LIKE }),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
expect(activityMock.search).toHaveBeenCalledWith({
|
||||
expect(mocks.activity.search).toHaveBeenCalledWith({
|
||||
assetId: 'asset-id',
|
||||
albumId: 'album-id',
|
||||
isLiked: true,
|
||||
|
@ -52,14 +47,14 @@ describe(ActivityService.name, () => {
|
|||
});
|
||||
|
||||
it('should filter by type=comment', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.activity.search.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id', type: ReactionType.COMMENT }),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
expect(activityMock.search).toHaveBeenCalledWith({
|
||||
expect(mocks.activity.search).toHaveBeenCalledWith({
|
||||
assetId: 'asset-id',
|
||||
albumId: 'album-id',
|
||||
isLiked: false,
|
||||
|
@ -69,8 +64,8 @@ describe(ActivityService.name, () => {
|
|||
|
||||
describe('getStatistics', () => {
|
||||
it('should get the comment count', async () => {
|
||||
activityMock.getStatistics.mockResolvedValue(1);
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([activityStub.oneComment.albumId]));
|
||||
mocks.activity.getStatistics.mockResolvedValue(1);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([activityStub.oneComment.albumId]));
|
||||
await expect(
|
||||
sut.getStatistics(authStub.admin, {
|
||||
assetId: 'asset-id',
|
||||
|
@ -93,8 +88,8 @@ describe(ActivityService.name, () => {
|
|||
});
|
||||
|
||||
it('should create a comment', async () => {
|
||||
accessMock.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id']));
|
||||
activityMock.create.mockResolvedValue(activityStub.oneComment);
|
||||
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.activity.create.mockResolvedValue(activityStub.oneComment);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
albumId: 'album-id',
|
||||
|
@ -103,7 +98,7 @@ describe(ActivityService.name, () => {
|
|||
comment: 'comment',
|
||||
});
|
||||
|
||||
expect(activityMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.activity.create).toHaveBeenCalledWith({
|
||||
userId: 'admin_id',
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
|
@ -113,8 +108,8 @@ describe(ActivityService.name, () => {
|
|||
});
|
||||
|
||||
it('should fail because activity is disabled for the album', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
activityMock.create.mockResolvedValue(activityStub.oneComment);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.activity.create.mockResolvedValue(activityStub.oneComment);
|
||||
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
|
@ -127,9 +122,9 @@ describe(ActivityService.name, () => {
|
|||
});
|
||||
|
||||
it('should create a like', async () => {
|
||||
accessMock.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id']));
|
||||
activityMock.create.mockResolvedValue(activityStub.liked);
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.activity.create.mockResolvedValue(activityStub.liked);
|
||||
mocks.activity.search.mockResolvedValue([]);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
albumId: 'album-id',
|
||||
|
@ -137,7 +132,7 @@ describe(ActivityService.name, () => {
|
|||
type: ReactionType.LIKE,
|
||||
});
|
||||
|
||||
expect(activityMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.activity.create).toHaveBeenCalledWith({
|
||||
userId: 'admin_id',
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
|
@ -146,9 +141,9 @@ describe(ActivityService.name, () => {
|
|||
});
|
||||
|
||||
it('should skip if like exists', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
accessMock.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id']));
|
||||
activityMock.search.mockResolvedValue([activityStub.liked]);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.activity.search.mockResolvedValue([activityStub.liked]);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
albumId: 'album-id',
|
||||
|
@ -156,26 +151,26 @@ describe(ActivityService.name, () => {
|
|||
type: ReactionType.LIKE,
|
||||
});
|
||||
|
||||
expect(activityMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.activity.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should require access', async () => {
|
||||
await expect(sut.delete(authStub.admin, activityStub.oneComment.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(activityMock.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.activity.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should let the activity owner delete a comment', async () => {
|
||||
accessMock.activity.checkOwnerAccess.mockResolvedValue(new Set(['activity-id']));
|
||||
mocks.access.activity.checkOwnerAccess.mockResolvedValue(new Set(['activity-id']));
|
||||
await sut.delete(authStub.admin, 'activity-id');
|
||||
expect(activityMock.delete).toHaveBeenCalledWith('activity-id');
|
||||
expect(mocks.activity.delete).toHaveBeenCalledWith('activity-id');
|
||||
});
|
||||
|
||||
it('should let the album owner delete a comment', async () => {
|
||||
accessMock.activity.checkAlbumOwnerAccess.mockResolvedValue(new Set(['activity-id']));
|
||||
mocks.access.activity.checkAlbumOwnerAccess.mockResolvedValue(new Set(['activity-id']));
|
||||
await sut.delete(authStub.admin, 'activity-id');
|
||||
expect(activityMock.delete).toHaveBeenCalledWith('activity-id');
|
||||
expect(mocks.activity.delete).toHaveBeenCalledWith('activity-id');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,29 +2,18 @@ import { BadRequestException } from '@nestjs/common';
|
|||
import _ from 'lodash';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { IAlbumUserRepository } from 'src/types';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(AlbumService.name, () => {
|
||||
let sut: AlbumService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let albumUserMock: Mocked<IAlbumUserRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, albumMock, albumUserMock, eventMock, userMock } = newTestService(AlbumService));
|
||||
({ sut, mocks } = newTestService(AlbumService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -33,25 +22,25 @@ describe(AlbumService.name, () => {
|
|||
|
||||
describe('getStatistics', () => {
|
||||
it('should get the album count', async () => {
|
||||
albumMock.getOwned.mockResolvedValue([]);
|
||||
albumMock.getShared.mockResolvedValue([]);
|
||||
albumMock.getNotShared.mockResolvedValue([]);
|
||||
mocks.album.getOwned.mockResolvedValue([]);
|
||||
mocks.album.getShared.mockResolvedValue([]);
|
||||
mocks.album.getNotShared.mockResolvedValue([]);
|
||||
await expect(sut.getStatistics(authStub.admin)).resolves.toEqual({
|
||||
owned: 0,
|
||||
shared: 0,
|
||||
notShared: 0,
|
||||
});
|
||||
|
||||
expect(albumMock.getOwned).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(albumMock.getShared).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(albumMock.getNotShared).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(mocks.album.getOwned).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(mocks.album.getShared).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(mocks.album.getNotShared).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('gets list of albums for auth user', async () => {
|
||||
albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
mocks.album.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null },
|
||||
{ albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null },
|
||||
]);
|
||||
|
@ -63,8 +52,8 @@ describe(AlbumService.name, () => {
|
|||
});
|
||||
|
||||
it('gets list of albums that have a specific asset', async () => {
|
||||
albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]);
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
mocks.album.getByAssetId.mockResolvedValue([albumStub.oneAsset]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: albumStub.oneAsset.id,
|
||||
assetCount: 1,
|
||||
|
@ -76,37 +65,37 @@ describe(AlbumService.name, () => {
|
|||
const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(albumStub.oneAsset.id);
|
||||
expect(albumMock.getByAssetId).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.getByAssetId).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('gets list of albums that are shared', async () => {
|
||||
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
mocks.album.getShared.mockResolvedValue([albumStub.sharedWithUser]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null },
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(authStub.admin, { shared: true });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(albumStub.sharedWithUser.id);
|
||||
expect(albumMock.getShared).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.getShared).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('gets list of albums that are NOT shared', async () => {
|
||||
albumMock.getNotShared.mockResolvedValue([albumStub.empty]);
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
mocks.album.getNotShared.mockResolvedValue([albumStub.empty]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null },
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(authStub.admin, { shared: false });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(albumStub.empty.id);
|
||||
expect(albumMock.getNotShared).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.getNotShared).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('counts assets correctly', async () => {
|
||||
albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]);
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
mocks.album.getOwned.mockResolvedValue([albumStub.oneAsset]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: albumStub.oneAsset.id,
|
||||
assetCount: 1,
|
||||
|
@ -119,14 +108,14 @@ describe(AlbumService.name, () => {
|
|||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].assetCount).toEqual(1);
|
||||
expect(albumMock.getOwned).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.getOwned).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('creates album', async () => {
|
||||
albumMock.create.mockResolvedValue(albumStub.empty);
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['123']));
|
||||
mocks.album.create.mockResolvedValue(albumStub.empty);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['123']));
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
albumName: 'Empty album',
|
||||
|
@ -135,7 +124,7 @@ describe(AlbumService.name, () => {
|
|||
assetIds: ['123'],
|
||||
});
|
||||
|
||||
expect(albumMock.create).toHaveBeenCalledWith(
|
||||
expect(mocks.album.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: albumStub.empty.albumName,
|
||||
|
@ -147,30 +136,30 @@ describe(AlbumService.name, () => {
|
|||
[{ userId: 'user-id', role: AlbumUserRole.EDITOR }],
|
||||
);
|
||||
|
||||
expect(userMock.get).toHaveBeenCalledWith('user-id', {});
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']));
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('album.invite', {
|
||||
expect(mocks.user.get).toHaveBeenCalledWith('user-id', {});
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']));
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
|
||||
id: albumStub.empty.id,
|
||||
userId: 'user-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should require valid userIds', async () => {
|
||||
userMock.get.mockResolvedValue(void 0);
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
albumName: 'Empty album',
|
||||
albumUsers: [{ userId: 'user-3', role: AlbumUserRole.EDITOR }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(userMock.get).toHaveBeenCalledWith('user-3', {});
|
||||
expect(albumMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.user.get).toHaveBeenCalledWith('user-3', {});
|
||||
expect(mocks.album.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only add assets the user is allowed to access', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
albumMock.create.mockResolvedValue(albumStub.oneAsset);
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.album.create.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
albumName: 'Test album',
|
||||
|
@ -178,7 +167,7 @@ describe(AlbumService.name, () => {
|
|||
assetIds: ['asset-1', 'asset-2'],
|
||||
});
|
||||
|
||||
expect(albumMock.create).toHaveBeenCalledWith(
|
||||
expect(mocks.album.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: 'Test album',
|
||||
|
@ -189,7 +178,7 @@ describe(AlbumService.name, () => {
|
|||
['asset-1'],
|
||||
[],
|
||||
);
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set(['asset-1', 'asset-2']),
|
||||
);
|
||||
|
@ -198,7 +187,7 @@ describe(AlbumService.name, () => {
|
|||
|
||||
describe('update', () => {
|
||||
it('should prevent updating an album that does not exist', async () => {
|
||||
albumMock.getById.mockResolvedValue(void 0);
|
||||
mocks.album.getById.mockResolvedValue(void 0);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.user1, 'invalid-id', {
|
||||
|
@ -206,7 +195,7 @@ describe(AlbumService.name, () => {
|
|||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent updating a not owned album (shared with auth user)', async () => {
|
||||
|
@ -218,10 +207,10 @@ describe(AlbumService.name, () => {
|
|||
});
|
||||
|
||||
it('should require a valid thumbnail asset id', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4']));
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
albumMock.update.mockResolvedValue(albumStub.oneAsset);
|
||||
albumMock.getAssetIds.mockResolvedValue(new Set());
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4']));
|
||||
mocks.album.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.album.update.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set());
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, albumStub.oneAsset.id, {
|
||||
|
@ -229,22 +218,22 @@ describe(AlbumService.name, () => {
|
|||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(albumMock.getAssetIds).toHaveBeenCalledWith('album-4', ['not-in-album']);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.getAssetIds).toHaveBeenCalledWith('album-4', ['not-in-album']);
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow the owner to update the album', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4']));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4']));
|
||||
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
albumMock.update.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.album.update.mockResolvedValue(albumStub.oneAsset);
|
||||
|
||||
await sut.update(authStub.admin, albumStub.oneAsset.id, {
|
||||
albumName: 'new album name',
|
||||
});
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-4', {
|
||||
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.update).toHaveBeenCalledWith('album-4', {
|
||||
id: 'album-4',
|
||||
albumName: 'new album name',
|
||||
});
|
||||
|
@ -253,33 +242,33 @@ describe(AlbumService.name, () => {
|
|||
|
||||
describe('delete', () => {
|
||||
it('should throw an error for an album not found', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(albumMock.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.album.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not let a shared user delete the album', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
|
||||
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(albumMock.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.album.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should let the owner delete an album', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.empty.id]));
|
||||
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.empty.id]));
|
||||
mocks.album.getById.mockResolvedValue(albumStub.empty);
|
||||
|
||||
await sut.delete(authStub.admin, albumStub.empty.id);
|
||||
|
||||
expect(albumMock.delete).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.delete).toHaveBeenCalledWith(albumStub.empty.id);
|
||||
expect(mocks.album.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.delete).toHaveBeenCalledWith(albumStub.empty.id);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -288,47 +277,47 @@ describe(AlbumService.name, () => {
|
|||
await expect(
|
||||
sut.addUsers(authStub.admin, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: 'user-1' }] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the userId is already added', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
mocks.album.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
await expect(
|
||||
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, {
|
||||
albumUsers: [{ userId: authStub.admin.user.id }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the userId does not exist', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
userMock.get.mockResolvedValue(void 0);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
mocks.album.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
await expect(
|
||||
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: 'user-3' }] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the userId is the ownerId', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
mocks.album.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
await expect(
|
||||
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, {
|
||||
albumUsers: [{ userId: userStub.user1.id }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add valid shared users', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
|
||||
albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
userMock.get.mockResolvedValue(userStub.user2);
|
||||
albumUserMock.create.mockResolvedValue({
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
|
||||
mocks.album.update.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
mocks.user.get.mockResolvedValue(userStub.user2);
|
||||
mocks.albumUser.create.mockResolvedValue({
|
||||
usersId: userStub.user2.id,
|
||||
albumsId: albumStub.sharedWithAdmin.id,
|
||||
role: AlbumUserRole.EDITOR,
|
||||
|
@ -336,11 +325,11 @@ describe(AlbumService.name, () => {
|
|||
await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, {
|
||||
albumUsers: [{ userId: authStub.user2.user.id }],
|
||||
});
|
||||
expect(albumUserMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.albumUser.create).toHaveBeenCalledWith({
|
||||
usersId: authStub.user2.user.id,
|
||||
albumsId: albumStub.sharedWithAdmin.id,
|
||||
});
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('album.invite', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
|
||||
id: albumStub.sharedWithAdmin.id,
|
||||
userId: userStub.user2.id,
|
||||
});
|
||||
|
@ -349,94 +338,94 @@ describe(AlbumService.name, () => {
|
|||
|
||||
describe('removeUser', () => {
|
||||
it('should require a valid album id', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
|
||||
albumMock.getById.mockResolvedValue(void 0);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
|
||||
mocks.album.getById.mockResolvedValue(void 0);
|
||||
await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove a shared user from an owned album', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithUser.id]));
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithUser.id]));
|
||||
mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||
|
||||
await expect(
|
||||
sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(albumUserMock.delete).toHaveBeenCalledTimes(1);
|
||||
expect(albumUserMock.delete).toHaveBeenCalledWith({
|
||||
expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.albumUser.delete).toHaveBeenCalledWith({
|
||||
albumsId: albumStub.sharedWithUser.id,
|
||||
usersId: userStub.user1.id,
|
||||
});
|
||||
expect(albumMock.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false });
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false });
|
||||
});
|
||||
|
||||
it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.sharedWithMultiple);
|
||||
|
||||
await expect(
|
||||
sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.user.id),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(albumUserMock.delete).not.toHaveBeenCalled();
|
||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.albumUser.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.user1.user.id,
|
||||
new Set([albumStub.sharedWithMultiple.id]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow a shared user to remove themselves', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||
|
||||
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.user.id);
|
||||
|
||||
expect(albumUserMock.delete).toHaveBeenCalledTimes(1);
|
||||
expect(albumUserMock.delete).toHaveBeenCalledWith({
|
||||
expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.albumUser.delete).toHaveBeenCalledWith({
|
||||
albumsId: albumStub.sharedWithUser.id,
|
||||
usersId: authStub.user1.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow a shared user to remove themselves using "me"', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser);
|
||||
|
||||
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me');
|
||||
|
||||
expect(albumUserMock.delete).toHaveBeenCalledTimes(1);
|
||||
expect(albumUserMock.delete).toHaveBeenCalledWith({
|
||||
expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.albumUser.delete).toHaveBeenCalledWith({
|
||||
albumsId: albumStub.sharedWithUser.id,
|
||||
usersId: authStub.user1.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow the owner to be removed', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.empty);
|
||||
|
||||
await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.user.id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error for a user not in the album', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.empty);
|
||||
|
||||
await expect(sut.removeUser(authStub.admin, albumStub.empty.id, 'user-3')).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('should update user role', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, {
|
||||
role: AlbumUserRole.EDITOR,
|
||||
});
|
||||
expect(albumUserMock.update).toHaveBeenCalledWith(
|
||||
expect(mocks.albumUser.update).toHaveBeenCalledWith(
|
||||
{ albumsId: albumStub.sharedWithAdmin.id, usersId: userStub.admin.id },
|
||||
{ role: AlbumUserRole.EDITOR },
|
||||
);
|
||||
|
@ -445,9 +434,9 @@ describe(AlbumService.name, () => {
|
|||
|
||||
describe('getAlbumInfo', () => {
|
||||
it('should get a shared album', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id]));
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
mocks.album.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id]));
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: albumStub.oneAsset.id,
|
||||
assetCount: 1,
|
||||
|
@ -458,17 +447,17 @@ describe(AlbumService.name, () => {
|
|||
|
||||
await sut.get(authStub.admin, albumStub.oneAsset.id, {});
|
||||
|
||||
expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true });
|
||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true });
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([albumStub.oneAsset.id]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should get a shared album via a shared link', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
mocks.album.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: albumStub.oneAsset.id,
|
||||
assetCount: 1,
|
||||
|
@ -479,17 +468,17 @@ describe(AlbumService.name, () => {
|
|||
|
||||
await sut.get(authStub.adminSharedLink, 'album-123', {});
|
||||
|
||||
expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
|
||||
expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
|
||||
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.sharedLink?.id,
|
||||
new Set(['album-123']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should get a shared album via shared with user', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
|
||||
albumMock.getMetadataForIds.mockResolvedValue([
|
||||
mocks.album.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: albumStub.oneAsset.id,
|
||||
assetCount: 1,
|
||||
|
@ -500,8 +489,8 @@ describe(AlbumService.name, () => {
|
|||
|
||||
await sut.get(authStub.user1, 'album-123', {});
|
||||
|
||||
expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
|
||||
expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
|
||||
expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
|
||||
authStub.user1.user.id,
|
||||
new Set(['album-123']),
|
||||
AlbumUserRole.VIEWER,
|
||||
|
@ -511,8 +500,8 @@ describe(AlbumService.name, () => {
|
|||
it('should throw an error for no access', async () => {
|
||||
await expect(sut.get(authStub.admin, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-123']));
|
||||
expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-123']));
|
||||
expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set(['album-123']),
|
||||
AlbumUserRole.VIEWER,
|
||||
|
@ -522,10 +511,10 @@ describe(AlbumService.name, () => {
|
|||
|
||||
describe('addAssets', () => {
|
||||
it('should allow the owner to add assets', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||
|
@ -535,37 +524,37 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
expect(mocks.album.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
});
|
||||
|
||||
it('should not set the thumbnail if the album has one already', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' }));
|
||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' }));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-1' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
expect(mocks.album.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-id',
|
||||
});
|
||||
expect(albumMock.addAssetIds).toHaveBeenCalled();
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow a shared user to add assets', async () => {
|
||||
accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser));
|
||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.user1, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||
|
@ -575,34 +564,34 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
expect(mocks.album.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('album.update', {
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('album.update', {
|
||||
id: 'album-123',
|
||||
recipientIds: ['admin_id'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow a shared user with viewer access to add assets', async () => {
|
||||
accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set([]));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser));
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([]));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser));
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.user2, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow a shared link user to add assets', async () => {
|
||||
accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||
|
@ -612,115 +601,115 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
expect(mocks.album.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
|
||||
expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.sharedLink?.id,
|
||||
new Set(['album-123']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow adding assets shared via partner sharing', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-1' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
expect(mocks.album.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
});
|
||||
|
||||
it('should skip duplicate assets', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
|
||||
|
||||
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip assets not shared with user', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.album.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
||||
{ success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION },
|
||||
]);
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
});
|
||||
|
||||
it('should not allow unauthorized access to the album', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalled();
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not allow unauthorized shared link access to the album', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalled();
|
||||
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeAssets', () => {
|
||||
it('should allow the owner to remove assets', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||
|
||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']);
|
||||
expect(mocks.album.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']);
|
||||
});
|
||||
|
||||
it('should skip assets not in the album', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty));
|
||||
albumMock.getAssetIds.mockResolvedValue(new Set());
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.empty));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: false, id: 'asset-id', error: BulkIdErrorReason.NOT_FOUND },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow owner to remove all assets from the album', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||
|
||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-id' },
|
||||
|
@ -728,16 +717,16 @@ describe(AlbumService.name, () => {
|
|||
});
|
||||
|
||||
it('should reset the thumbnail if it is removed', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
|
||||
albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||
|
||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.updateThumbnails).toHaveBeenCalled();
|
||||
expect(mocks.album.updateThumbnails).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,50 +1,45 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Permission } from 'src/enum';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { APIKeyService } from 'src/services/api-key.service';
|
||||
import { IApiKeyRepository } from 'src/types';
|
||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(APIKeyService.name, () => {
|
||||
let sut: APIKeyService;
|
||||
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let keyMock: Mocked<IApiKeyRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, cryptoMock, keyMock } = newTestService(APIKeyService));
|
||||
({ sut, mocks } = newTestService(APIKeyService));
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new key', async () => {
|
||||
keyMock.create.mockResolvedValue(keyStub.admin);
|
||||
mocks.apiKey.create.mockResolvedValue(keyStub.admin);
|
||||
await sut.create(authStub.admin, { name: 'Test Key', permissions: [Permission.ALL] });
|
||||
expect(keyMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.apiKey.create).toHaveBeenCalledWith({
|
||||
key: 'cmFuZG9tLWJ5dGVz (hashed)',
|
||||
name: 'Test Key',
|
||||
permissions: [Permission.ALL],
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
expect(cryptoMock.newPassword).toHaveBeenCalled();
|
||||
expect(cryptoMock.hashSha256).toHaveBeenCalled();
|
||||
expect(mocks.crypto.newPassword).toHaveBeenCalled();
|
||||
expect(mocks.crypto.hashSha256).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not require a name', async () => {
|
||||
keyMock.create.mockResolvedValue(keyStub.admin);
|
||||
mocks.apiKey.create.mockResolvedValue(keyStub.admin);
|
||||
|
||||
await sut.create(authStub.admin, { permissions: [Permission.ALL] });
|
||||
|
||||
expect(keyMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.apiKey.create).toHaveBeenCalledWith({
|
||||
key: 'cmFuZG9tLWJ5dGVz (hashed)',
|
||||
name: 'API Key',
|
||||
permissions: [Permission.ALL],
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
expect(cryptoMock.newPassword).toHaveBeenCalled();
|
||||
expect(cryptoMock.hashSha256).toHaveBeenCalled();
|
||||
expect(mocks.crypto.newPassword).toHaveBeenCalled();
|
||||
expect(mocks.crypto.hashSha256).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the api key does not have sufficient permissions', async () => {
|
||||
|
@ -60,16 +55,16 @@ describe(APIKeyService.name, () => {
|
|||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(keyMock.update).not.toHaveBeenCalledWith('random-guid');
|
||||
expect(mocks.apiKey.update).not.toHaveBeenCalledWith('random-guid');
|
||||
});
|
||||
|
||||
it('should update a key', async () => {
|
||||
keyMock.getById.mockResolvedValue(keyStub.admin);
|
||||
keyMock.update.mockResolvedValue(keyStub.admin);
|
||||
mocks.apiKey.getById.mockResolvedValue(keyStub.admin);
|
||||
mocks.apiKey.update.mockResolvedValue(keyStub.admin);
|
||||
|
||||
await sut.update(authStub.admin, 'random-guid', { name: 'New Name' });
|
||||
|
||||
expect(keyMock.update).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid', { name: 'New Name' });
|
||||
expect(mocks.apiKey.update).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid', { name: 'New Name' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -77,15 +72,15 @@ describe(APIKeyService.name, () => {
|
|||
it('should throw an error if the key is not found', async () => {
|
||||
await expect(sut.delete(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(keyMock.delete).not.toHaveBeenCalledWith('random-guid');
|
||||
expect(mocks.apiKey.delete).not.toHaveBeenCalledWith('random-guid');
|
||||
});
|
||||
|
||||
it('should delete a key', async () => {
|
||||
keyMock.getById.mockResolvedValue(keyStub.admin);
|
||||
mocks.apiKey.getById.mockResolvedValue(keyStub.admin);
|
||||
|
||||
await sut.delete(authStub.admin, 'random-guid');
|
||||
|
||||
expect(keyMock.delete).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
|
||||
expect(mocks.apiKey.delete).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -93,25 +88,25 @@ describe(APIKeyService.name, () => {
|
|||
it('should throw an error if the key is not found', async () => {
|
||||
await expect(sut.getById(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
|
||||
expect(mocks.apiKey.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
|
||||
});
|
||||
|
||||
it('should get a key by id', async () => {
|
||||
keyMock.getById.mockResolvedValue(keyStub.admin);
|
||||
mocks.apiKey.getById.mockResolvedValue(keyStub.admin);
|
||||
|
||||
await sut.getById(authStub.admin, 'random-guid');
|
||||
|
||||
expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
|
||||
expect(mocks.apiKey.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all the keys for a user', async () => {
|
||||
keyMock.getByUserId.mockResolvedValue([keyStub.admin]);
|
||||
mocks.apiKey.getByUserId.mockResolvedValue([keyStub.admin]);
|
||||
|
||||
await expect(sut.getAll(authStub.admin)).resolves.toHaveLength(1);
|
||||
|
||||
expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(mocks.apiKey.getByUserId).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,10 +10,7 @@ import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldN
|
|||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetFileType, AssetStatus, AssetType, CacheControl } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { JobName } from 'src/interfaces/job.interface';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
|
@ -21,9 +18,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
|||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
|
||||
|
@ -203,15 +198,10 @@ const copiedAsset = Object.freeze({
|
|||
|
||||
describe(AssetMediaService.name, () => {
|
||||
let sut: AssetMediaService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, assetMock, jobMock, storageMock, userMock } = newTestService(AssetMediaService));
|
||||
({ sut, mocks } = newTestService(AssetMediaService));
|
||||
});
|
||||
|
||||
describe('getUploadAssetIdByChecksum', () => {
|
||||
|
@ -221,25 +211,25 @@ describe(AssetMediaService.name, () => {
|
|||
|
||||
it('should handle a non-existent asset', async () => {
|
||||
await expect(sut.getUploadAssetIdByChecksum(authStub.admin, file1.toString('hex'))).resolves.toBeUndefined();
|
||||
expect(assetMock.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
|
||||
expect(mocks.asset.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
|
||||
});
|
||||
|
||||
it('should find an existing asset', async () => {
|
||||
assetMock.getUploadAssetIdByChecksum.mockResolvedValue('asset-id');
|
||||
mocks.asset.getUploadAssetIdByChecksum.mockResolvedValue('asset-id');
|
||||
await expect(sut.getUploadAssetIdByChecksum(authStub.admin, file1.toString('hex'))).resolves.toEqual({
|
||||
id: 'asset-id',
|
||||
status: AssetMediaStatus.DUPLICATE,
|
||||
});
|
||||
expect(assetMock.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
|
||||
expect(mocks.asset.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
|
||||
});
|
||||
|
||||
it('should find an existing asset by base64', async () => {
|
||||
assetMock.getUploadAssetIdByChecksum.mockResolvedValue('asset-id');
|
||||
mocks.asset.getUploadAssetIdByChecksum.mockResolvedValue('asset-id');
|
||||
await expect(sut.getUploadAssetIdByChecksum(authStub.admin, file1.toString('base64'))).resolves.toEqual({
|
||||
id: 'asset-id',
|
||||
status: AssetMediaStatus.DUPLICATE,
|
||||
});
|
||||
expect(assetMock.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
|
||||
expect(mocks.asset.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -308,14 +298,14 @@ describe(AssetMediaService.name, () => {
|
|||
expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.PROFILE_DATA, 'image.jpg'))).toEqual(
|
||||
'upload/profile/admin_id',
|
||||
);
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile/admin_id');
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/profile/admin_id');
|
||||
});
|
||||
|
||||
it('should return upload for everything else', () => {
|
||||
expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.ASSET_DATA, 'image.jpg'))).toEqual(
|
||||
'upload/upload/admin_id/ra/nd',
|
||||
);
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/upload/admin_id/ra/nd');
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/upload/admin_id/ra/nd');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -330,7 +320,7 @@ describe(AssetMediaService.name, () => {
|
|||
size: 42,
|
||||
};
|
||||
|
||||
assetMock.create.mockResolvedValue(assetEntity);
|
||||
mocks.asset.create.mockResolvedValue(assetEntity);
|
||||
|
||||
await expect(
|
||||
sut.uploadAsset(
|
||||
|
@ -340,9 +330,9 @@ describe(AssetMediaService.name, () => {
|
|||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(assetMock.create).not.toHaveBeenCalled();
|
||||
expect(userMock.updateUsage).not.toHaveBeenCalledWith(authStub.user1.user.id, file.size);
|
||||
expect(storageMock.utimes).not.toHaveBeenCalledWith(
|
||||
expect(mocks.asset.create).not.toHaveBeenCalled();
|
||||
expect(mocks.user.updateUsage).not.toHaveBeenCalledWith(authStub.user1.user.id, file.size);
|
||||
expect(mocks.storage.utimes).not.toHaveBeenCalledWith(
|
||||
file.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(createDto.fileModifiedAt),
|
||||
|
@ -359,16 +349,16 @@ describe(AssetMediaService.name, () => {
|
|||
size: 42,
|
||||
};
|
||||
|
||||
assetMock.create.mockResolvedValue(assetEntity);
|
||||
mocks.asset.create.mockResolvedValue(assetEntity);
|
||||
|
||||
await expect(sut.uploadAsset(authStub.user1, createDto, file)).resolves.toEqual({
|
||||
id: 'id_1',
|
||||
status: AssetMediaStatus.CREATED,
|
||||
});
|
||||
|
||||
expect(assetMock.create).toHaveBeenCalled();
|
||||
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size);
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.create).toHaveBeenCalled();
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size);
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
file.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(createDto.fileModifiedAt),
|
||||
|
@ -387,19 +377,19 @@ describe(AssetMediaService.name, () => {
|
|||
const error = new Error('unique key violation');
|
||||
(error as any).constraint_name = ASSET_CHECKSUM_CONSTRAINT;
|
||||
|
||||
assetMock.create.mockRejectedValue(error);
|
||||
assetMock.getUploadAssetIdByChecksum.mockResolvedValue(assetEntity.id);
|
||||
mocks.asset.create.mockRejectedValue(error);
|
||||
mocks.asset.getUploadAssetIdByChecksum.mockResolvedValue(assetEntity.id);
|
||||
|
||||
await expect(sut.uploadAsset(authStub.user1, createDto, file)).resolves.toEqual({
|
||||
id: 'id_1',
|
||||
status: AssetMediaStatus.DUPLICATE,
|
||||
});
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.DELETE_FILES,
|
||||
data: { files: ['fake_path/asset_1.jpeg', undefined] },
|
||||
});
|
||||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
||||
expect(mocks.user.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the duplicate could not be found by checksum', async () => {
|
||||
|
@ -414,22 +404,22 @@ describe(AssetMediaService.name, () => {
|
|||
const error = new Error('unique key violation');
|
||||
(error as any).constraint_name = ASSET_CHECKSUM_CONSTRAINT;
|
||||
|
||||
assetMock.create.mockRejectedValue(error);
|
||||
mocks.asset.create.mockRejectedValue(error);
|
||||
|
||||
await expect(sut.uploadAsset(authStub.user1, createDto, file)).rejects.toBeInstanceOf(
|
||||
InternalServerErrorException,
|
||||
);
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.DELETE_FILES,
|
||||
data: { files: ['fake_path/asset_1.jpeg', undefined] },
|
||||
});
|
||||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
||||
expect(mocks.user.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a live photo', async () => {
|
||||
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
|
||||
await expect(
|
||||
sut.uploadAsset(
|
||||
|
@ -442,13 +432,13 @@ describe(AssetMediaService.name, () => {
|
|||
id: 'live-photo-still-asset',
|
||||
});
|
||||
|
||||
expect(assetMock.getById).toHaveBeenCalledWith('live-photo-motion-asset');
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset');
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should hide the linked motion asset', async () => {
|
||||
assetMock.getById.mockResolvedValueOnce({ ...assetStub.livePhotoMotionAsset, isVisible: true });
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce({ ...assetStub.livePhotoMotionAsset, isVisible: true });
|
||||
mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
|
||||
await expect(
|
||||
sut.uploadAsset(
|
||||
|
@ -461,25 +451,25 @@ describe(AssetMediaService.name, () => {
|
|||
id: 'live-photo-still-asset',
|
||||
});
|
||||
|
||||
expect(assetMock.getById).toHaveBeenCalledWith('live-photo-motion-asset');
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false });
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset');
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false });
|
||||
});
|
||||
|
||||
it('should handle a sidecar file', async () => {
|
||||
assetMock.getById.mockResolvedValueOnce(assetStub.image);
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.asset.create.mockResolvedValueOnce(assetStub.image);
|
||||
|
||||
await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({
|
||||
status: AssetMediaStatus.CREATED,
|
||||
id: assetStub.image.id,
|
||||
});
|
||||
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
fileStub.photoSidecar.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(createDto.fileModifiedAt),
|
||||
);
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -487,22 +477,22 @@ describe(AssetMediaService.name, () => {
|
|||
it('should require the asset.download permission', async () => {
|
||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
});
|
||||
|
||||
it('should throw an error if the asset is not found', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException);
|
||||
|
||||
expect(assetMock.getById).toHaveBeenCalledWith('asset-1', { files: true });
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith('asset-1', { files: true });
|
||||
});
|
||||
|
||||
it('should download a file', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
|
@ -518,13 +508,13 @@ describe(AssetMediaService.name, () => {
|
|||
it('should require asset.view permissions', async () => {
|
||||
await expect(sut.viewThumbnail(authStub.admin, 'id', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
});
|
||||
|
||||
it('should throw an error if the asset does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
||||
|
@ -532,8 +522,8 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the requested thumbnail file does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({ ...assetStub.image, files: [] });
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue({ ...assetStub.image, files: [] });
|
||||
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||
|
@ -541,8 +531,8 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the requested preview file does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
files: [
|
||||
{
|
||||
|
@ -561,8 +551,8 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should fall back to preview if the requested thumbnail file does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
files: [
|
||||
{
|
||||
|
@ -589,8 +579,8 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should get preview file', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({ ...assetStub.image });
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue({ ...assetStub.image });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
||||
).resolves.toEqual(
|
||||
|
@ -604,8 +594,8 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should get thumbnail file', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({ ...assetStub.image });
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue({ ...assetStub.image });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||
).resolves.toEqual(
|
||||
|
@ -623,27 +613,27 @@ describe(AssetMediaService.name, () => {
|
|||
it('should require asset.view permissions', async () => {
|
||||
await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
});
|
||||
|
||||
it('should throw an error if the asset does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw an error if the asset is not a video', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should return the encoded video path if available', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.hasEncodedVideo);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.hasEncodedVideo);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
|
@ -655,8 +645,8 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should fall back to the original path', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.video);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.video);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
|
@ -670,12 +660,12 @@ describe(AssetMediaService.name, () => {
|
|||
|
||||
describe('checkExistingAssets', () => {
|
||||
it('should get existing asset ids', async () => {
|
||||
assetMock.getByDeviceIds.mockResolvedValue(['42']);
|
||||
mocks.asset.getByDeviceIds.mockResolvedValue(['42']);
|
||||
await expect(
|
||||
sut.checkExistingAssets(authStub.admin, { deviceId: '420', deviceAssetIds: ['69'] }),
|
||||
).resolves.toEqual({ existingIds: ['42'] });
|
||||
|
||||
expect(assetMock.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']);
|
||||
expect(mocks.asset.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -685,26 +675,26 @@ describe(AssetMediaService.name, () => {
|
|||
'Not found or no asset.update access',
|
||||
);
|
||||
|
||||
expect(assetMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update a photo with no sidecar to photo with no sidecar', async () => {
|
||||
const updatedFile = fileStub.photo;
|
||||
const updatedAsset = { ...existingAsset, ...updatedFile };
|
||||
assetMock.getById.mockResolvedValueOnce(existingAsset);
|
||||
assetMock.getById.mockResolvedValueOnce(updatedAsset);
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([existingAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(existingAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([existingAsset.id]));
|
||||
// this is the original file size
|
||||
storageMock.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
// this is for the clone call
|
||||
assetMock.create.mockResolvedValue(copiedAsset);
|
||||
mocks.asset.create.mockResolvedValue(copiedAsset);
|
||||
|
||||
await expect(sut.replaceAsset(authStub.user1, existingAsset.id, replaceDto, updatedFile)).resolves.toEqual({
|
||||
status: AssetMediaStatus.REPLACED,
|
||||
id: 'copied-asset',
|
||||
});
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: existingAsset.id,
|
||||
sidecarPath: null,
|
||||
|
@ -712,7 +702,7 @@ describe(AssetMediaService.name, () => {
|
|||
originalPath: 'fake_path/photo1.jpeg',
|
||||
}),
|
||||
);
|
||||
expect(assetMock.create).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sidecarPath: null,
|
||||
originalFileName: 'existing-filename.jpeg',
|
||||
|
@ -720,12 +710,12 @@ describe(AssetMediaService.name, () => {
|
|||
}),
|
||||
);
|
||||
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
deletedAt: expect.any(Date),
|
||||
status: AssetStatus.TRASHED,
|
||||
});
|
||||
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
updatedFile.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(replaceDto.fileModifiedAt),
|
||||
|
@ -736,13 +726,13 @@ describe(AssetMediaService.name, () => {
|
|||
const updatedFile = fileStub.photo;
|
||||
const sidecarFile = fileStub.photoSidecar;
|
||||
const updatedAsset = { ...sidecarAsset, ...updatedFile };
|
||||
assetMock.getById.mockResolvedValueOnce(existingAsset);
|
||||
assetMock.getById.mockResolvedValueOnce(updatedAsset);
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(existingAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
// this is the original file size
|
||||
storageMock.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
// this is for the clone call
|
||||
assetMock.create.mockResolvedValue(copiedAsset);
|
||||
mocks.asset.create.mockResolvedValue(copiedAsset);
|
||||
|
||||
await expect(
|
||||
sut.replaceAsset(authStub.user1, sidecarAsset.id, replaceDto, updatedFile, sidecarFile),
|
||||
|
@ -751,12 +741,12 @@ describe(AssetMediaService.name, () => {
|
|||
id: 'copied-asset',
|
||||
});
|
||||
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
deletedAt: expect.any(Date),
|
||||
status: AssetStatus.TRASHED,
|
||||
});
|
||||
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
updatedFile.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(replaceDto.fileModifiedAt),
|
||||
|
@ -767,25 +757,25 @@ describe(AssetMediaService.name, () => {
|
|||
const updatedFile = fileStub.photo;
|
||||
|
||||
const updatedAsset = { ...sidecarAsset, ...updatedFile };
|
||||
assetMock.getById.mockResolvedValueOnce(sidecarAsset);
|
||||
assetMock.getById.mockResolvedValueOnce(updatedAsset);
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(sidecarAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
// this is the original file size
|
||||
storageMock.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
// this is for the copy call
|
||||
assetMock.create.mockResolvedValue(copiedAsset);
|
||||
mocks.asset.create.mockResolvedValue(copiedAsset);
|
||||
|
||||
await expect(sut.replaceAsset(authStub.user1, existingAsset.id, replaceDto, updatedFile)).resolves.toEqual({
|
||||
status: AssetMediaStatus.REPLACED,
|
||||
id: 'copied-asset',
|
||||
});
|
||||
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
deletedAt: expect.any(Date),
|
||||
status: AssetStatus.TRASHED,
|
||||
});
|
||||
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
updatedFile.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(replaceDto.fileModifiedAt),
|
||||
|
@ -797,27 +787,27 @@ describe(AssetMediaService.name, () => {
|
|||
const error = new Error('unique key violation');
|
||||
(error as any).constraint_name = ASSET_CHECKSUM_CONSTRAINT;
|
||||
|
||||
assetMock.update.mockRejectedValue(error);
|
||||
assetMock.getById.mockResolvedValueOnce(sidecarAsset);
|
||||
assetMock.getUploadAssetIdByChecksum.mockResolvedValue(sidecarAsset.id);
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
mocks.asset.update.mockRejectedValue(error);
|
||||
mocks.asset.getById.mockResolvedValueOnce(sidecarAsset);
|
||||
mocks.asset.getUploadAssetIdByChecksum.mockResolvedValue(sidecarAsset.id);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
// this is the original file size
|
||||
storageMock.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
// this is for the clone call
|
||||
assetMock.create.mockResolvedValue(copiedAsset);
|
||||
mocks.asset.create.mockResolvedValue(copiedAsset);
|
||||
|
||||
await expect(sut.replaceAsset(authStub.user1, sidecarAsset.id, replaceDto, updatedFile)).resolves.toEqual({
|
||||
status: AssetMediaStatus.DUPLICATE,
|
||||
id: sidecarAsset.id,
|
||||
});
|
||||
|
||||
expect(assetMock.create).not.toHaveBeenCalled();
|
||||
expect(assetMock.updateAll).not.toHaveBeenCalled();
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.create).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.updateAll).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.DELETE_FILES,
|
||||
data: { files: [updatedFile.originalPath, undefined] },
|
||||
});
|
||||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
||||
expect(mocks.user.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -826,7 +816,7 @@ describe(AssetMediaService.name, () => {
|
|||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
||||
|
||||
assetMock.getByChecksums.mockResolvedValue([
|
||||
mocks.asset.getByChecksums.mockResolvedValue([
|
||||
{ id: 'asset-1', checksum: file1 } as AssetEntity,
|
||||
{ id: 'asset-2', checksum: file2 } as AssetEntity,
|
||||
]);
|
||||
|
@ -857,14 +847,14 @@ describe(AssetMediaService.name, () => {
|
|||
],
|
||||
});
|
||||
|
||||
expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
expect(mocks.asset.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
});
|
||||
|
||||
it('should return non-duplicates as well', async () => {
|
||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
||||
|
||||
assetMock.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1 } as AssetEntity]);
|
||||
mocks.asset.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1 } as AssetEntity]);
|
||||
|
||||
await expect(
|
||||
sut.bulkUploadCheck(authStub.admin, {
|
||||
|
@ -889,7 +879,7 @@ describe(AssetMediaService.name, () => {
|
|||
],
|
||||
});
|
||||
|
||||
expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
expect(mocks.asset.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -910,7 +900,7 @@ describe(AssetMediaService.name, () => {
|
|||
|
||||
await sut.onUploadError(request, file);
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.DELETE_FILES,
|
||||
data: { files: ['upload/upload/user-id/ra/nd/random-uuid.jpg'] },
|
||||
});
|
||||
|
|
|
@ -4,22 +4,16 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
|||
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, AssetType } from 'src/enum';
|
||||
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AssetStats } from 'src/interfaces/asset.interface';
|
||||
import { JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { ISystemMetadataRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { vitest } from 'vitest';
|
||||
|
||||
const stats: AssetStats = {
|
||||
[AssetType.IMAGE]: 10,
|
||||
|
@ -36,27 +30,18 @@ const statResponse: AssetStatsResponseDto = {
|
|||
|
||||
describe(AssetService.name, () => {
|
||||
let sut: AssetService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let partnerMock: Mocked<IPartnerRepository>;
|
||||
let stackMock: Mocked<IStackRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
const mockGetById = (assets: AssetEntity[]) => {
|
||||
assetMock.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId)));
|
||||
mocks.asset.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId)));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, assetMock, eventMock, jobMock, partnerMock, stackMock, systemMock, userMock } =
|
||||
newTestService(AssetService));
|
||||
({ sut, mocks } = newTestService(AssetService));
|
||||
|
||||
mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]);
|
||||
});
|
||||
|
@ -77,8 +62,8 @@ describe(AssetService.name, () => {
|
|||
const image3 = { ...assetStub.image, localDateTime: new Date(2015, 1, 15) };
|
||||
const image4 = { ...assetStub.image, localDateTime: new Date(2009, 1, 15) };
|
||||
|
||||
partnerMock.getAll.mockResolvedValue([]);
|
||||
assetMock.getByDayOfYear.mockResolvedValue([
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getByDayOfYear.mockResolvedValue([
|
||||
{
|
||||
yearsAgo: 1,
|
||||
assets: [image1, image2],
|
||||
|
@ -99,16 +84,16 @@ describe(AssetService.name, () => {
|
|||
{ yearsAgo: 15, title: '15 years ago', assets: [mapAsset(image4)] },
|
||||
]);
|
||||
|
||||
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]);
|
||||
expect(mocks.asset.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]);
|
||||
});
|
||||
|
||||
it('should get memories with partners with inTimeline enabled', async () => {
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
|
||||
assetMock.getByDayOfYear.mockResolvedValue([]);
|
||||
mocks.partner.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
|
||||
mocks.asset.getByDayOfYear.mockResolvedValue([]);
|
||||
|
||||
await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 });
|
||||
|
||||
expect(assetMock.getByDayOfYear.mock.calls).toEqual([
|
||||
expect(mocks.asset.getByDayOfYear.mock.calls).toEqual([
|
||||
[[authStub.admin.user.id, userStub.user1.id], { day: 15, month: 1 }],
|
||||
]);
|
||||
});
|
||||
|
@ -116,76 +101,76 @@ describe(AssetService.name, () => {
|
|||
|
||||
describe('getStatistics', () => {
|
||||
it('should get the statistics for a user, excluding archived assets', async () => {
|
||||
assetMock.getStatistics.mockResolvedValue(stats);
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse);
|
||||
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false });
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false });
|
||||
});
|
||||
|
||||
it('should get the statistics for a user for archived assets', async () => {
|
||||
assetMock.getStatistics.mockResolvedValue(stats);
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse);
|
||||
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true });
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true });
|
||||
});
|
||||
|
||||
it('should get the statistics for a user for favorite assets', async () => {
|
||||
assetMock.getStatistics.mockResolvedValue(stats);
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse);
|
||||
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isFavorite: true });
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isFavorite: true });
|
||||
});
|
||||
|
||||
it('should get the statistics for a user for all assets', async () => {
|
||||
assetMock.getStatistics.mockResolvedValue(stats);
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse);
|
||||
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {});
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRandom', () => {
|
||||
it('should get own random assets', async () => {
|
||||
assetMock.getRandom.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
|
||||
await sut.getRandom(authStub.admin, 1);
|
||||
expect(assetMock.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
|
||||
expect(mocks.asset.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
|
||||
});
|
||||
|
||||
it('should not include partner assets if not in timeline', async () => {
|
||||
assetMock.getRandom.mockResolvedValue([assetStub.image]);
|
||||
partnerMock.getAll.mockResolvedValue([{ ...partnerStub.user1ToAdmin1, inTimeline: false }]);
|
||||
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
|
||||
mocks.partner.getAll.mockResolvedValue([{ ...partnerStub.user1ToAdmin1, inTimeline: false }]);
|
||||
await sut.getRandom(authStub.admin, 1);
|
||||
expect(assetMock.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
|
||||
expect(mocks.asset.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
|
||||
});
|
||||
|
||||
it('should include partner assets if in timeline', async () => {
|
||||
assetMock.getRandom.mockResolvedValue([assetStub.image]);
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
|
||||
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
|
||||
mocks.partner.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
|
||||
await sut.getRandom(authStub.admin, 1);
|
||||
expect(assetMock.getRandom).toHaveBeenCalledWith([userStub.admin.id, userStub.user1.id], 1);
|
||||
expect(mocks.asset.getRandom).toHaveBeenCalledWith([userStub.admin.id, userStub.user1.id], 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should allow owner access', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
await sut.get(authStub.admin, assetStub.image.id);
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow shared link access', async () => {
|
||||
accessMock.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
await sut.get(authStub.adminSharedLink, assetStub.image.id);
|
||||
expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.sharedLink?.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip metadata for shared link if exif is disabled', async () => {
|
||||
accessMock.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
|
||||
const result = await sut.get(
|
||||
{ ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
|
||||
|
@ -194,27 +179,27 @@ describe(AssetService.name, () => {
|
|||
|
||||
expect(result).toEqual(expect.objectContaining({ hasMetadata: false }));
|
||||
expect(result).not.toHaveProperty('exifInfo');
|
||||
expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.sharedLink?.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow partner sharing access', async () => {
|
||||
accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
await sut.get(authStub.admin, assetStub.image.id);
|
||||
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow shared album access', async () => {
|
||||
accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
await sut.get(authStub.admin, assetStub.image.id);
|
||||
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
|
@ -222,17 +207,17 @@ describe(AssetService.name, () => {
|
|||
|
||||
it('should throw an error for no access', async () => {
|
||||
await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(assetMock.getById).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error for an invalid shared link', async () => {
|
||||
await expect(sut.get(authStub.adminSharedLink, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
|
||||
expect(assetMock.getById).not.toHaveBeenCalled();
|
||||
expect(mocks.access.asset.checkOwnerAccess).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the asset could not be found', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
@ -242,40 +227,40 @@ describe(AssetService.name, () => {
|
|||
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update the asset', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
assetMock.update.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValue(assetStub.image);
|
||||
|
||||
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
|
||||
});
|
||||
|
||||
it('should update the exif description', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
assetMock.update.mockResolvedValue(assetStub.image);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValue(assetStub.image);
|
||||
|
||||
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
|
||||
|
||||
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
|
||||
});
|
||||
|
||||
it('should update the exif rating', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
assetMock.getById.mockResolvedValueOnce(assetStub.image);
|
||||
assetMock.update.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValueOnce(assetStub.image);
|
||||
|
||||
await sut.update(authStub.admin, 'asset-1', { rating: 3 });
|
||||
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', rating: 3 });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', rating: 3 });
|
||||
});
|
||||
|
||||
it('should fail linking a live video if the motion part could not be found', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
||||
|
@ -283,20 +268,20 @@ describe(AssetService.name, () => {
|
|||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(assetMock.update).not.toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking a live video if the motion part is not a video', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
||||
|
@ -304,20 +289,20 @@ describe(AssetService.name, () => {
|
|||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(assetMock.update).not.toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking a live video if the motion part has a different owner', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
||||
|
@ -325,79 +310,79 @@ describe(AssetService.name, () => {
|
|||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(assetMock.update).not.toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should link a live video', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
assetMock.getById.mockResolvedValueOnce({
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
ownerId: authStub.admin.user.id,
|
||||
isVisible: true,
|
||||
});
|
||||
assetMock.getById.mockResolvedValueOnce(assetStub.image);
|
||||
assetMock.update.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValue(assetStub.image);
|
||||
|
||||
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('asset.hide', {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if asset could not be found after update', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await expect(sut.update(authStub.admin, 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should unlink a live video', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
assetMock.update.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.update.mockResolvedValueOnce(assetStub.image);
|
||||
|
||||
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: null,
|
||||
});
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('asset.show', {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail unlinking a live video if the asset could not be found', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
assetMock.getById.mockResolvedValueOnce(undefined);
|
||||
mocks.asset.getById.mockResolvedValueOnce(undefined);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(eventMock.emit).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -412,13 +397,13 @@ describe(AssetService.name, () => {
|
|||
});
|
||||
|
||||
it('should update all assets', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||
});
|
||||
|
||||
it('should not update Assets table if no relevant fields are provided', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await sut.updateAll(authStub.admin, {
|
||||
ids: ['asset-1'],
|
||||
latitude: 0,
|
||||
|
@ -428,11 +413,11 @@ describe(AssetService.name, () => {
|
|||
duplicateId: undefined,
|
||||
rating: undefined,
|
||||
});
|
||||
expect(assetMock.updateAll).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.updateAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update Assets table if isArchived field is provided', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await sut.updateAll(authStub.admin, {
|
||||
ids: ['asset-1'],
|
||||
latitude: 0,
|
||||
|
@ -442,7 +427,7 @@ describe(AssetService.name, () => {
|
|||
duplicateId: undefined,
|
||||
rating: undefined,
|
||||
});
|
||||
expect(assetMock.updateAll).toHaveBeenCalled();
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -456,26 +441,26 @@ describe(AssetService.name, () => {
|
|||
});
|
||||
|
||||
it('should force delete a batch of assets', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
|
||||
|
||||
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
|
||||
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('assets.delete', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('assets.delete', {
|
||||
assetIds: ['asset1', 'asset2'],
|
||||
userId: 'user-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should soft delete a batch of assets', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
|
||||
|
||||
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: false });
|
||||
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset1', 'asset2'], {
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset1', 'asset2'], {
|
||||
deletedAt: expect.any(Date),
|
||||
status: AssetStatus.TRASHED,
|
||||
});
|
||||
expect(jobMock.queue.mock.calls).toEqual([]);
|
||||
expect(mocks.job.queue.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -489,27 +474,27 @@ describe(AssetService.name, () => {
|
|||
});
|
||||
|
||||
it('should immediately queue assets for deletion if trash is disabled', async () => {
|
||||
assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
|
||||
systemMock.get.mockResolvedValue({ trash: { enabled: false } });
|
||||
mocks.asset.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: false } });
|
||||
|
||||
await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalledWith(expect.anything(), { trashedBefore: new Date() });
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
expect(mocks.asset.getAll).toHaveBeenCalledWith(expect.anything(), { trashedBefore: new Date() });
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should queue assets for deletion after trash duration', async () => {
|
||||
assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
|
||||
systemMock.get.mockResolvedValue({ trash: { enabled: true, days: 7 } });
|
||||
mocks.asset.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: true, days: 7 } });
|
||||
|
||||
await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalledWith(expect.anything(), {
|
||||
expect(mocks.asset.getAll).toHaveBeenCalledWith(expect.anything(), {
|
||||
trashedBefore: DateTime.now().minus({ days: 7 }).toJSDate(),
|
||||
});
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
|
||||
]);
|
||||
});
|
||||
|
@ -519,11 +504,11 @@ describe(AssetService.name, () => {
|
|||
it('should remove faces', async () => {
|
||||
const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
|
||||
|
||||
assetMock.getById.mockResolvedValue(assetWithFace);
|
||||
mocks.asset.getById.mockResolvedValue(assetWithFace);
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true });
|
||||
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
expect(mocks.job.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.DELETE_FILES,
|
||||
|
@ -540,41 +525,41 @@ describe(AssetService.name, () => {
|
|||
],
|
||||
]);
|
||||
|
||||
expect(assetMock.remove).toHaveBeenCalledWith(assetWithFace);
|
||||
expect(mocks.asset.remove).toHaveBeenCalledWith(assetWithFace);
|
||||
});
|
||||
|
||||
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||
|
||||
expect(stackMock.update).toHaveBeenCalledWith('stack-1', {
|
||||
expect(mocks.stack.update).toHaveBeenCalledWith('stack-1', {
|
||||
id: 'stack-1',
|
||||
primaryAssetId: 'stack-child-asset-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
|
||||
assetMock.getById.mockResolvedValue({
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...assetStub.primaryImage,
|
||||
stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },
|
||||
} as AssetEntity);
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||
|
||||
expect(stackMock.delete).toHaveBeenCalledWith('stack-1');
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith('stack-1');
|
||||
});
|
||||
|
||||
it('should delete a live photo', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
assetMock.getLivePhotoCount.mockResolvedValue(0);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
||||
|
||||
await sut.handleAssetDeletion({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
deleteOnDisk: true,
|
||||
});
|
||||
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
expect(mocks.job.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.ASSET_DELETION,
|
||||
|
@ -596,15 +581,15 @@ describe(AssetService.name, () => {
|
|||
});
|
||||
|
||||
it('should not delete a live motion part if it is being used by another asset', async () => {
|
||||
assetMock.getLivePhotoCount.mockResolvedValue(2);
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
|
||||
await sut.handleAssetDeletion({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
deleteOnDisk: true,
|
||||
});
|
||||
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
expect(mocks.job.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.DELETE_FILES,
|
||||
|
@ -617,9 +602,9 @@ describe(AssetService.name, () => {
|
|||
});
|
||||
|
||||
it('should update usage', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
|
||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
||||
});
|
||||
|
||||
it('should fail if asset could not be found', async () => {
|
||||
|
@ -631,27 +616,27 @@ describe(AssetService.name, () => {
|
|||
|
||||
describe('run', () => {
|
||||
it('should run the refresh faces job', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_FACES });
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }]);
|
||||
});
|
||||
|
||||
it('should run the refresh metadata job', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA });
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }]);
|
||||
});
|
||||
|
||||
it('should run the refresh thumbnails job', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL });
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }]);
|
||||
});
|
||||
|
||||
it('should run the transcode video', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO });
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } }]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -659,7 +644,7 @@ describe(AssetService.name, () => {
|
|||
it('get assets by device id', async () => {
|
||||
const assets = [assetStub.image, assetStub.image1];
|
||||
|
||||
assetMock.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId));
|
||||
mocks.asset.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId));
|
||||
|
||||
const deviceId = 'device-id';
|
||||
const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId);
|
||||
|
|
|
@ -1,28 +1,18 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { FileReportItemDto } from 'src/dtos/audit.dto';
|
||||
import { AssetFileType, AssetPathType, DatabaseAction, EntityType, PersonPathType, UserPathType } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AuditService } from 'src/services/audit.service';
|
||||
import { IAuditRepository } from 'src/types';
|
||||
import { auditStub } from 'test/fixtures/audit.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(AuditService.name, () => {
|
||||
let sut: AuditService;
|
||||
let auditMock: Mocked<IAuditRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, auditMock, assetMock, cryptoMock, personMock, userMock } = newTestService(AuditService));
|
||||
({ sut, mocks } = newTestService(AuditService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -32,13 +22,13 @@ describe(AuditService.name, () => {
|
|||
describe('handleCleanup', () => {
|
||||
it('should delete old audit entries', async () => {
|
||||
await expect(sut.handleCleanup()).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(auditMock.removeBefore).toHaveBeenCalledWith(expect.any(Date));
|
||||
expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeletes', () => {
|
||||
it('should require full sync if the request is older than 100 days', async () => {
|
||||
auditMock.getAfter.mockResolvedValue([]);
|
||||
mocks.audit.getAfter.mockResolvedValue([]);
|
||||
|
||||
const date = new Date(2022, 0, 1);
|
||||
await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({
|
||||
|
@ -46,7 +36,7 @@ describe(AuditService.name, () => {
|
|||
ids: [],
|
||||
});
|
||||
|
||||
expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
|
||||
expect(mocks.audit.getAfter).toHaveBeenCalledWith(date, {
|
||||
action: DatabaseAction.DELETE,
|
||||
userIds: [authStub.admin.user.id],
|
||||
entityType: EntityType.ASSET,
|
||||
|
@ -54,7 +44,7 @@ describe(AuditService.name, () => {
|
|||
});
|
||||
|
||||
it('should get any new or updated assets and deleted ids', async () => {
|
||||
auditMock.getAfter.mockResolvedValue([auditStub.delete.entityId]);
|
||||
mocks.audit.getAfter.mockResolvedValue([auditStub.delete.entityId]);
|
||||
|
||||
const date = new Date();
|
||||
await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({
|
||||
|
@ -62,7 +52,7 @@ describe(AuditService.name, () => {
|
|||
ids: ['asset-deleted'],
|
||||
});
|
||||
|
||||
expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
|
||||
expect(mocks.audit.getAfter).toHaveBeenCalledWith(date, {
|
||||
action: DatabaseAction.DELETE,
|
||||
userIds: [authStub.admin.user.id],
|
||||
entityType: EntityType.ASSET,
|
||||
|
@ -74,7 +64,7 @@ describe(AuditService.name, () => {
|
|||
it('should fail if the file is not in the immich path', async () => {
|
||||
await expect(sut.getChecksums({ filenames: ['foo/bar'] })).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(cryptoMock.hashFile).not.toHaveBeenCalled();
|
||||
expect(mocks.crypto.hashFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should get checksum for valid file', async () => {
|
||||
|
@ -82,7 +72,7 @@ describe(AuditService.name, () => {
|
|||
{ filename: './upload/my-file.jpg', checksum: expect.any(String) },
|
||||
]);
|
||||
|
||||
expect(cryptoMock.hashFile).toHaveBeenCalledWith('./upload/my-file.jpg');
|
||||
expect(mocks.crypto.hashFile).toHaveBeenCalledWith('./upload/my-file.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -94,10 +84,10 @@ describe(AuditService.name, () => {
|
|||
]),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(assetMock.upsertFile).not.toHaveBeenCalled();
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update encoded video path', async () => {
|
||||
|
@ -109,10 +99,10 @@ describe(AuditService.name, () => {
|
|||
} as FileReportItemDto,
|
||||
]);
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'my-id', encodedVideoPath: './upload/my-video.mp4' });
|
||||
expect(assetMock.upsertFile).not.toHaveBeenCalled();
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', encodedVideoPath: './upload/my-video.mp4' });
|
||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update preview path', async () => {
|
||||
|
@ -124,14 +114,14 @@ describe(AuditService.name, () => {
|
|||
} as FileReportItemDto,
|
||||
]);
|
||||
|
||||
expect(assetMock.upsertFile).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.upsertFile).toHaveBeenCalledWith({
|
||||
assetId: 'my-id',
|
||||
type: AssetFileType.PREVIEW,
|
||||
path: './upload/my-preview.png',
|
||||
});
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update thumbnail path', async () => {
|
||||
|
@ -143,14 +133,14 @@ describe(AuditService.name, () => {
|
|||
} as FileReportItemDto,
|
||||
]);
|
||||
|
||||
expect(assetMock.upsertFile).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.upsertFile).toHaveBeenCalledWith({
|
||||
assetId: 'my-id',
|
||||
type: AssetFileType.THUMBNAIL,
|
||||
path: './upload/my-thumbnail.webp',
|
||||
});
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update original path', async () => {
|
||||
|
@ -162,10 +152,10 @@ describe(AuditService.name, () => {
|
|||
} as FileReportItemDto,
|
||||
]);
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'my-id', originalPath: './upload/my-original.png' });
|
||||
expect(assetMock.upsertFile).not.toHaveBeenCalled();
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', originalPath: './upload/my-original.png' });
|
||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update sidecar path', async () => {
|
||||
|
@ -177,10 +167,10 @@ describe(AuditService.name, () => {
|
|||
} as FileReportItemDto,
|
||||
]);
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'my-id', sidecarPath: './upload/my-sidecar.xmp' });
|
||||
expect(assetMock.upsertFile).not.toHaveBeenCalled();
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', sidecarPath: './upload/my-sidecar.xmp' });
|
||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update face path', async () => {
|
||||
|
@ -192,10 +182,10 @@ describe(AuditService.name, () => {
|
|||
} as FileReportItemDto,
|
||||
]);
|
||||
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'my-id', thumbnailPath: './upload/my-face.jpg' });
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(assetMock.upsertFile).not.toHaveBeenCalled();
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'my-id', thumbnailPath: './upload/my-face.jpg' });
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update profile path', async () => {
|
||||
|
@ -207,10 +197,10 @@ describe(AuditService.name, () => {
|
|||
} as FileReportItemDto,
|
||||
]);
|
||||
|
||||
expect(userMock.update).toHaveBeenCalledWith('my-id', { profileImagePath: './upload/my-profile-pic.jpg' });
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(assetMock.upsertFile).not.toHaveBeenCalled();
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).toHaveBeenCalledWith('my-id', { profileImagePath: './upload/my-profile-pic.jpg' });
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,20 +3,14 @@ import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
|||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AuthType, Permission } from 'src/enum';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { IApiKeyRepository, IOAuthRepository, ISessionRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sessionStub } from 'test/fixtures/session.stub';
|
||||
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const oauthResponse = {
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
|
@ -56,23 +50,14 @@ const oauthUserWithDefaultQuota = {
|
|||
|
||||
describe('AuthService', () => {
|
||||
let sut: AuthService;
|
||||
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let keyMock: Mocked<IApiKeyRepository>;
|
||||
let oauthMock: Mocked<IOAuthRepository>;
|
||||
let sessionMock: Mocked<ISessionRepository>;
|
||||
let sharedLinkMock: Mocked<ISharedLinkRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, cryptoMock, eventMock, keyMock, oauthMock, sessionMock, sharedLinkMock, systemMock, userMock } =
|
||||
newTestService(AuthService));
|
||||
({ sut, mocks } = newTestService(AuthService));
|
||||
|
||||
oauthMock.authorize.mockResolvedValue('access-token');
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email });
|
||||
oauthMock.getLogoutEndpoint.mockResolvedValue('http://end-session-endpoint');
|
||||
mocks.oauth.authorize.mockResolvedValue('access-token');
|
||||
mocks.oauth.getProfile.mockResolvedValue({ sub, email });
|
||||
mocks.oauth.getLogoutEndpoint.mockResolvedValue('http://end-session-endpoint');
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
@ -82,31 +67,31 @@ describe('AuthService', () => {
|
|||
describe('onBootstrap', () => {
|
||||
it('should init the repo', () => {
|
||||
sut.onBootstrap();
|
||||
expect(oauthMock.init).toHaveBeenCalled();
|
||||
expect(mocks.oauth.init).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should throw an error if password login is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.disabled);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.disabled);
|
||||
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should check the user exists', async () => {
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should check the user has a password', async () => {
|
||||
userMock.getByEmail.mockResolvedValue({} as UserEntity);
|
||||
mocks.user.getByEmail.mockResolvedValue({} as UserEntity);
|
||||
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should successfully log the user in', async () => {
|
||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
mocks.user.getByEmail.mockResolvedValue(userStub.user1);
|
||||
mocks.session.create.mockResolvedValue(sessionStub.valid);
|
||||
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual({
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
userId: 'user-id',
|
||||
|
@ -116,7 +101,7 @@ describe('AuthService', () => {
|
|||
isAdmin: false,
|
||||
shouldChangePassword: false,
|
||||
});
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -125,23 +110,23 @@ describe('AuthService', () => {
|
|||
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
|
||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||
|
||||
userMock.getByEmail.mockResolvedValue({
|
||||
mocks.user.getByEmail.mockResolvedValue({
|
||||
email: 'test@immich.com',
|
||||
password: 'hash-password',
|
||||
} as UserEntity);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await sut.changePassword(auth, dto);
|
||||
|
||||
expect(userMock.getByEmail).toHaveBeenCalledWith(auth.user.email, true);
|
||||
expect(cryptoMock.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, true);
|
||||
expect(mocks.crypto.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
|
||||
});
|
||||
|
||||
it('should throw when auth user email is not found', async () => {
|
||||
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
|
||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
|
||||
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
@ -150,9 +135,9 @@ describe('AuthService', () => {
|
|||
const auth = { user: { email: 'test@imimch.com' } as UserEntity };
|
||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||
|
||||
cryptoMock.compareBcrypt.mockReturnValue(false);
|
||||
mocks.crypto.compareBcrypt.mockReturnValue(false);
|
||||
|
||||
userMock.getByEmail.mockResolvedValue({
|
||||
mocks.user.getByEmail.mockResolvedValue({
|
||||
email: 'test@immich.com',
|
||||
password: 'hash-password',
|
||||
} as UserEntity);
|
||||
|
@ -164,7 +149,7 @@ describe('AuthService', () => {
|
|||
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
|
||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||
|
||||
userMock.getByEmail.mockResolvedValue({
|
||||
mocks.user.getByEmail.mockResolvedValue({
|
||||
email: 'test@immich.com',
|
||||
password: '',
|
||||
} as UserEntity);
|
||||
|
@ -175,7 +160,7 @@ describe('AuthService', () => {
|
|||
|
||||
describe('logout', () => {
|
||||
it('should return the end session endpoint', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
const auth = { user: { id: '123' } } as AuthDto;
|
||||
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
|
||||
successful: true,
|
||||
|
@ -200,8 +185,8 @@ describe('AuthService', () => {
|
|||
redirectUri: '/auth/login?autoLaunch=0',
|
||||
});
|
||||
|
||||
expect(sessionMock.delete).toHaveBeenCalledWith('token123');
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('session.delete', { sessionId: 'token123' });
|
||||
expect(mocks.session.delete).toHaveBeenCalledWith('token123');
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('session.delete', { sessionId: 'token123' });
|
||||
});
|
||||
|
||||
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
|
||||
|
@ -218,14 +203,14 @@ describe('AuthService', () => {
|
|||
const dto: SignUpDto = { email: 'test@immich.com', password: 'password', name: 'immich admin' };
|
||||
|
||||
it('should only allow one admin', async () => {
|
||||
userMock.getAdmin.mockResolvedValue({} as UserEntity);
|
||||
mocks.user.getAdmin.mockResolvedValue({} as UserEntity);
|
||||
await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(userMock.getAdmin).toHaveBeenCalled();
|
||||
expect(mocks.user.getAdmin).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should sign up the admin', async () => {
|
||||
userMock.getAdmin.mockResolvedValue(void 0);
|
||||
userMock.create.mockResolvedValue({
|
||||
mocks.user.getAdmin.mockResolvedValue(void 0);
|
||||
mocks.user.create.mockResolvedValue({
|
||||
...dto,
|
||||
id: 'admin',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
|
@ -238,8 +223,8 @@ describe('AuthService', () => {
|
|||
email: 'test@immich.com',
|
||||
name: 'immich admin',
|
||||
});
|
||||
expect(userMock.getAdmin).toHaveBeenCalled();
|
||||
expect(userMock.create).toHaveBeenCalled();
|
||||
expect(mocks.user.getAdmin).toHaveBeenCalled();
|
||||
expect(mocks.user.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -255,8 +240,8 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should validate using authorization header', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { authorization: 'Bearer auth_token' },
|
||||
|
@ -282,7 +267,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should not accept an expired key', async () => {
|
||||
sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-immich-share-key': 'key' },
|
||||
|
@ -293,7 +278,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should not accept a key on a non-shared route', async () => {
|
||||
sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-immich-share-key': 'key' },
|
||||
|
@ -304,8 +289,8 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should not accept a key without a user', async () => {
|
||||
sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
||||
userMock.get.mockResolvedValue(void 0);
|
||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-immich-share-key': 'key' },
|
||||
|
@ -316,8 +301,8 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should accept a base64url key', async () => {
|
||||
sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') },
|
||||
|
@ -328,12 +313,12 @@ describe('AuthService', () => {
|
|||
user: userStub.admin,
|
||||
sharedLink: sharedLinkStub.valid,
|
||||
});
|
||||
expect(sharedLinkMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
|
||||
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
|
||||
});
|
||||
|
||||
it('should accept a hex key', async () => {
|
||||
sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') },
|
||||
|
@ -344,13 +329,13 @@ describe('AuthService', () => {
|
|||
user: userStub.admin,
|
||||
sharedLink: sharedLinkStub.valid,
|
||||
});
|
||||
expect(sharedLinkMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
|
||||
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate - user token', () => {
|
||||
it('should throw if no token is found', async () => {
|
||||
sessionMock.getByToken.mockResolvedValue(void 0);
|
||||
mocks.session.getByToken.mockResolvedValue(void 0);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-immich-user-token': 'auth_token' },
|
||||
|
@ -361,7 +346,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should return an auth dto', async () => {
|
||||
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
|
||||
mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { cookie: 'immich_access_token=auth_token' },
|
||||
|
@ -375,7 +360,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should throw if admin route and not an admin', async () => {
|
||||
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
|
||||
mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { cookie: 'immich_access_token=auth_token' },
|
||||
|
@ -386,8 +371,8 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should update when access time exceeds an hour', async () => {
|
||||
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive as any);
|
||||
sessionMock.update.mockResolvedValue(sessionStub.valid);
|
||||
mocks.session.getByToken.mockResolvedValue(sessionStub.inactive as any);
|
||||
mocks.session.update.mockResolvedValue(sessionStub.valid);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { cookie: 'immich_access_token=auth_token' },
|
||||
|
@ -395,13 +380,13 @@ describe('AuthService', () => {
|
|||
metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' },
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
expect(sessionMock.update.mock.calls[0][1]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) });
|
||||
expect(mocks.session.update.mock.calls[0][1]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate - api key', () => {
|
||||
it('should throw an error if no api key is found', async () => {
|
||||
keyMock.getKey.mockResolvedValue(void 0);
|
||||
mocks.apiKey.getKey.mockResolvedValue(void 0);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-api-key': 'auth_token' },
|
||||
|
@ -409,11 +394,11 @@ describe('AuthService', () => {
|
|||
metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' },
|
||||
}),
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
|
||||
expect(mocks.apiKey.getKey).toHaveBeenCalledWith('auth_token (hashed)');
|
||||
});
|
||||
|
||||
it('should throw an error if api key has insufficient permissions', async () => {
|
||||
keyMock.getKey.mockResolvedValue(keyStub.authKey);
|
||||
mocks.apiKey.getKey.mockResolvedValue(keyStub.authKey);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-api-key': 'auth_token' },
|
||||
|
@ -424,7 +409,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should return an auth dto', async () => {
|
||||
keyMock.getKey.mockResolvedValue(keyStub.authKey);
|
||||
mocks.apiKey.getKey.mockResolvedValue(keyStub.authKey);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-api-key': 'auth_token' },
|
||||
|
@ -432,7 +417,7 @@ describe('AuthService', () => {
|
|||
metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' },
|
||||
}),
|
||||
).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.authKey });
|
||||
expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
|
||||
expect(mocks.apiKey.getKey).toHaveBeenCalledWith('auth_token (hashed)');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -450,14 +435,14 @@ describe('AuthService', () => {
|
|||
|
||||
describe('authorize', () => {
|
||||
it('should fail if oauth is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({ oauth: { enabled: false } });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ oauth: { enabled: false } });
|
||||
await expect(sut.authorize({ redirectUri: 'https://demo.immich.app' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should authorize the user', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride);
|
||||
await sut.authorize({ redirectUri: 'https://demo.immich.app' });
|
||||
});
|
||||
});
|
||||
|
@ -468,71 +453,71 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should not allow auto registering', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should link an existing user', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
mocks.user.getByEmail.mockResolvedValue(userStub.user1);
|
||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||
mocks.session.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { oauthId: sub });
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(userStub.user1.id, { oauthId: sub });
|
||||
});
|
||||
|
||||
it('should not link to a user with a different oauth sub', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
|
||||
userMock.getByEmail.mockResolvedValueOnce({ ...userStub.user1, oauthId: 'existing-sub' });
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
|
||||
mocks.user.getByEmail.mockResolvedValueOnce({ ...userStub.user1, oauthId: 'existing-sub' });
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(userMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow auto registering by default', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
mocks.session.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create
|
||||
expect(userMock.create).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create
|
||||
expect(mocks.user.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw an error if user should be auto registered but the email claim does not exist', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email: undefined });
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
mocks.session.create.mockResolvedValue(sessionStub.valid);
|
||||
mocks.oauth.getProfile.mockResolvedValue({ sub, email: undefined });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(userMock.getByEmail).not.toHaveBeenCalled();
|
||||
expect(userMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.user.getByEmail).not.toHaveBeenCalled();
|
||||
expect(mocks.user.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
for (const url of [
|
||||
|
@ -544,68 +529,68 @@ describe('AuthService', () => {
|
|||
'app.immich:///oauth-callback?code=abc123',
|
||||
]) {
|
||||
it(`should use the mobile redirect override for a url of ${url}`, async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride);
|
||||
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride);
|
||||
mocks.user.getByOAuthId.mockResolvedValue(userStub.user1);
|
||||
mocks.session.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
await sut.callback({ url }, loginDetails);
|
||||
expect(oauthMock.getProfile).toHaveBeenCalledWith(expect.objectContaining({}), url, 'http://mobile-redirect');
|
||||
expect(mocks.oauth.getProfile).toHaveBeenCalledWith(expect.objectContaining({}), url, 'http://mobile-redirect');
|
||||
});
|
||||
}
|
||||
|
||||
it('should use the default quota', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
|
||||
expect(mocks.user.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
|
||||
});
|
||||
|
||||
it('should ignore an invalid storage quota', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' });
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
|
||||
expect(mocks.user.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
|
||||
});
|
||||
|
||||
it('should ignore a negative quota', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 });
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
|
||||
expect(mocks.user.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
|
||||
});
|
||||
|
||||
it('should not set quota for 0 quota', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 });
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||
email,
|
||||
name: ' ',
|
||||
oauthId: sub,
|
||||
|
@ -615,17 +600,17 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should use a valid storage quota', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 });
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||
email,
|
||||
name: ' ',
|
||||
oauthId: sub,
|
||||
|
@ -637,34 +622,34 @@ describe('AuthService', () => {
|
|||
|
||||
describe('link', () => {
|
||||
it('should link an account', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' });
|
||||
|
||||
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: sub });
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: sub });
|
||||
});
|
||||
|
||||
it('should not link an already linked oauth.sub', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.getByOAuthId.mockResolvedValue({ id: 'other-user' } as UserEntity);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
mocks.user.getByOAuthId.mockResolvedValue({ id: 'other-user' } as UserEntity);
|
||||
|
||||
await expect(sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unlink', () => {
|
||||
it('should unlink an account', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await sut.unlink(authStub.user1);
|
||||
|
||||
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: '' });
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: '' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,27 +2,18 @@ import { PassThrough } from 'node:stream';
|
|||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { ImmichWorker, StorageFolder } from 'src/enum';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { BackupService } from 'src/services/backup.service';
|
||||
import { IConfigRepository, ICronRepository, IProcessRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { mockSpawn, newTestService } from 'test/utils';
|
||||
import { describe, Mocked } from 'vitest';
|
||||
import { mockSpawn, newTestService, ServiceMocks } from 'test/utils';
|
||||
import { describe } from 'vitest';
|
||||
|
||||
describe(BackupService.name, () => {
|
||||
let sut: BackupService;
|
||||
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let cronMock: Mocked<ICronRepository>;
|
||||
let processMock: Mocked<IProcessRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, cronMock, configMock, databaseMock, processMock, storageMock, systemMock } = newTestService(BackupService));
|
||||
({ sut, mocks } = newTestService(BackupService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -31,32 +22,32 @@ describe(BackupService.name, () => {
|
|||
|
||||
describe('onBootstrapEvent', () => {
|
||||
it('should init cron job and handle config changes', async () => {
|
||||
databaseMock.tryLock.mockResolvedValue(true);
|
||||
mocks.database.tryLock.mockResolvedValue(true);
|
||||
|
||||
await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
|
||||
|
||||
expect(cronMock.create).toHaveBeenCalled();
|
||||
expect(mocks.cron.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not initialize backup database cron job when lock is taken', async () => {
|
||||
databaseMock.tryLock.mockResolvedValue(false);
|
||||
mocks.database.tryLock.mockResolvedValue(false);
|
||||
|
||||
await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
|
||||
|
||||
expect(cronMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.cron.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not initialise backup database job when running on microservices', async () => {
|
||||
configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||
mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||
await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
|
||||
|
||||
expect(cronMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.cron.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onConfigUpdateEvent', () => {
|
||||
beforeEach(async () => {
|
||||
databaseMock.tryLock.mockResolvedValue(true);
|
||||
mocks.database.tryLock.mockResolvedValue(true);
|
||||
await sut.onConfigInit({ newConfig: defaults });
|
||||
});
|
||||
|
||||
|
@ -73,66 +64,66 @@ describe(BackupService.name, () => {
|
|||
} as SystemConfig,
|
||||
});
|
||||
|
||||
expect(cronMock.update).toHaveBeenCalledWith({ name: 'backupDatabase', expression: '0 1 * * *', start: true });
|
||||
expect(cronMock.update).toHaveBeenCalled();
|
||||
expect(mocks.cron.update).toHaveBeenCalledWith({ name: 'backupDatabase', expression: '0 1 * * *', start: true });
|
||||
expect(mocks.cron.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing if instance does not have the backup database lock', async () => {
|
||||
databaseMock.tryLock.mockResolvedValue(false);
|
||||
mocks.database.tryLock.mockResolvedValue(false);
|
||||
await sut.onConfigInit({ newConfig: defaults });
|
||||
sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig, oldConfig: defaults });
|
||||
expect(cronMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.cron.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupDatabaseBackups', () => {
|
||||
it('should do nothing if not reached keepLastAmount', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
storageMock.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz']);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz']);
|
||||
await sut.cleanupDatabaseBackups();
|
||||
expect(storageMock.unlink).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.unlink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove failed backup files', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
storageMock.readdir.mockResolvedValue([
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
mocks.storage.readdir.mockResolvedValue([
|
||||
'immich-db-backup-123.sql.gz.tmp',
|
||||
'immich-db-backup-234.sql.gz',
|
||||
'immich-db-backup-345.sql.gz.tmp',
|
||||
]);
|
||||
await sut.cleanupDatabaseBackups();
|
||||
expect(storageMock.unlink).toHaveBeenCalledTimes(2);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(
|
||||
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-123.sql.gz.tmp`,
|
||||
);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(
|
||||
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-345.sql.gz.tmp`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove old backup files over keepLastAmount', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
storageMock.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz', 'immich-db-backup-2.sql.gz']);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz', 'immich-db-backup-2.sql.gz']);
|
||||
await sut.cleanupDatabaseBackups();
|
||||
expect(storageMock.unlink).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(
|
||||
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-1.sql.gz`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove old backup files over keepLastAmount and failed backups', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
storageMock.readdir.mockResolvedValue([
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
mocks.storage.readdir.mockResolvedValue([
|
||||
'immich-db-backup-1.sql.gz.tmp',
|
||||
'immich-db-backup-2.sql.gz',
|
||||
'immich-db-backup-3.sql.gz',
|
||||
]);
|
||||
await sut.cleanupDatabaseBackups();
|
||||
expect(storageMock.unlink).toHaveBeenCalledTimes(2);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(
|
||||
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-1.sql.gz.tmp`,
|
||||
);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(
|
||||
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-2.sql.gz`,
|
||||
);
|
||||
});
|
||||
|
@ -140,57 +131,57 @@ describe(BackupService.name, () => {
|
|||
|
||||
describe('handleBackupDatabase', () => {
|
||||
beforeEach(() => {
|
||||
storageMock.readdir.mockResolvedValue([]);
|
||||
processMock.spawn.mockReturnValue(mockSpawn(0, 'data', ''));
|
||||
storageMock.rename.mockResolvedValue();
|
||||
storageMock.unlink.mockResolvedValue();
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
storageMock.createWriteStream.mockReturnValue(new PassThrough());
|
||||
mocks.storage.readdir.mockResolvedValue([]);
|
||||
mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', ''));
|
||||
mocks.storage.rename.mockResolvedValue();
|
||||
mocks.storage.unlink.mockResolvedValue();
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
|
||||
});
|
||||
it('should run a database backup successfully', async () => {
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(storageMock.createWriteStream).toHaveBeenCalled();
|
||||
expect(mocks.storage.createWriteStream).toHaveBeenCalled();
|
||||
});
|
||||
it('should rename file on success', async () => {
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(storageMock.rename).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalled();
|
||||
});
|
||||
it('should fail if pg_dumpall fails', async () => {
|
||||
processMock.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
});
|
||||
it('should not rename file if pgdump fails and gzip succeeds', async () => {
|
||||
processMock.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should fail if gzip fails', async () => {
|
||||
processMock.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
|
||||
processMock.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
});
|
||||
it('should fail if write stream fails', async () => {
|
||||
storageMock.createWriteStream.mockImplementation(() => {
|
||||
mocks.storage.createWriteStream.mockImplementation(() => {
|
||||
throw new Error('error');
|
||||
});
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
});
|
||||
it('should fail if rename fails', async () => {
|
||||
storageMock.rename.mockRejectedValue(new Error('error'));
|
||||
mocks.storage.rename.mockRejectedValue(new Error('error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
});
|
||||
it('should ignore unlink failing and still return failed job status', async () => {
|
||||
processMock.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
storageMock.unlink.mockRejectedValue(new Error('error'));
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
mocks.storage.unlink.mockRejectedValue(new Error('error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(storageMock.unlink).toHaveBeenCalled();
|
||||
expect(mocks.storage.unlink).toHaveBeenCalled();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
});
|
||||
it.each`
|
||||
|
@ -204,9 +195,9 @@ describe(BackupService.name, () => {
|
|||
`(
|
||||
`should use pg_dumpall $expectedVersion with postgres version $postgresVersion`,
|
||||
async ({ postgresVersion, expectedVersion }) => {
|
||||
databaseMock.getPostgresVersion.mockResolvedValue(postgresVersion);
|
||||
mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion);
|
||||
await sut.handleBackupDatabase();
|
||||
expect(processMock.spawn).toHaveBeenCalledWith(
|
||||
expect(mocks.process.spawn).toHaveBeenCalledWith(
|
||||
`/usr/lib/postgresql/${expectedVersion}/bin/pg_dumpall`,
|
||||
expect.any(Array),
|
||||
expect.any(Object),
|
||||
|
@ -218,9 +209,9 @@ describe(BackupService.name, () => {
|
|||
${'13.99.99'}
|
||||
${'18.0.0'}
|
||||
`(`should fail if postgres version $postgresVersion is not supported`, async ({ postgresVersion }) => {
|
||||
databaseMock.getPostgresVersion.mockResolvedValue(postgresVersion);
|
||||
mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion);
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(processMock.spawn).not.toHaveBeenCalled();
|
||||
expect(mocks.process.spawn).not.toHaveBeenCalled();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,6 +30,7 @@ import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
|||
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CronRepository } from 'src/repositories/cron.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MapRepository } from 'src/repositories/map.repository';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
|
@ -61,7 +62,7 @@ export class BaseService {
|
|||
@Inject(IAssetRepository) protected assetRepository: IAssetRepository,
|
||||
protected configRepository: ConfigRepository,
|
||||
protected cronRepository: CronRepository,
|
||||
@Inject(ICryptoRepository) protected cryptoRepository: ICryptoRepository,
|
||||
@Inject(ICryptoRepository) protected cryptoRepository: CryptoRepository,
|
||||
@Inject(IDatabaseRepository) protected databaseRepository: IDatabaseRepository,
|
||||
@Inject(IEventRepository) protected eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) protected jobRepository: IJobRepository,
|
||||
|
|
|
@ -1,31 +1,27 @@
|
|||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
import { ISystemMetadataRepository } from 'src/types';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked, describe, it } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
describe(CliService.name, () => {
|
||||
let sut: CliService;
|
||||
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, userMock, systemMock } = newTestService(CliService));
|
||||
({ sut, mocks } = newTestService(CliService));
|
||||
});
|
||||
|
||||
describe('listUsers', () => {
|
||||
it('should list users', async () => {
|
||||
userMock.getList.mockResolvedValue([userStub.admin]);
|
||||
mocks.user.getList.mockResolvedValue([userStub.admin]);
|
||||
await expect(sut.listUsers()).resolves.toEqual([expect.objectContaining({ isAdmin: true })]);
|
||||
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true });
|
||||
expect(mocks.user.getList).toHaveBeenCalledWith({ withDeleted: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetAdminPassword', () => {
|
||||
it('should only work when there is an admin account', async () => {
|
||||
userMock.getAdmin.mockResolvedValue(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValue(void 0);
|
||||
const ask = vitest.fn().mockResolvedValue('new-password');
|
||||
|
||||
await expect(sut.resetAdminPassword(ask)).rejects.toThrowError('Admin account does not exist');
|
||||
|
@ -34,12 +30,12 @@ describe(CliService.name, () => {
|
|||
});
|
||||
|
||||
it('should default to a random password', async () => {
|
||||
userMock.getAdmin.mockResolvedValue(userStub.admin);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.admin);
|
||||
const ask = vitest.fn().mockImplementation(() => {});
|
||||
|
||||
const response = await sut.resetAdminPassword(ask);
|
||||
|
||||
const [id, update] = userMock.update.mock.calls[0];
|
||||
const [id, update] = mocks.user.update.mock.calls[0];
|
||||
|
||||
expect(response.provided).toBe(false);
|
||||
expect(ask).toHaveBeenCalled();
|
||||
|
@ -48,12 +44,12 @@ describe(CliService.name, () => {
|
|||
});
|
||||
|
||||
it('should use the supplied password', async () => {
|
||||
userMock.getAdmin.mockResolvedValue(userStub.admin);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.admin);
|
||||
const ask = vitest.fn().mockResolvedValue('new-password');
|
||||
|
||||
const response = await sut.resetAdminPassword(ask);
|
||||
|
||||
const [id, update] = userMock.update.mock.calls[0];
|
||||
const [id, update] = mocks.user.update.mock.calls[0];
|
||||
|
||||
expect(response.provided).toBe(true);
|
||||
expect(ask).toHaveBeenCalled();
|
||||
|
@ -65,28 +61,28 @@ describe(CliService.name, () => {
|
|||
describe('disablePasswordLogin', () => {
|
||||
it('should disable password login', async () => {
|
||||
await sut.disablePasswordLogin();
|
||||
expect(systemMock.set).toHaveBeenCalledWith('system-config', { passwordLogin: { enabled: false } });
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith('system-config', { passwordLogin: { enabled: false } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('enablePasswordLogin', () => {
|
||||
it('should enable password login', async () => {
|
||||
await sut.enablePasswordLogin();
|
||||
expect(systemMock.set).toHaveBeenCalledWith('system-config', {});
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith('system-config', {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableOAuthLogin', () => {
|
||||
it('should disable oauth login', async () => {
|
||||
await sut.disableOAuthLogin();
|
||||
expect(systemMock.set).toHaveBeenCalledWith('system-config', {});
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith('system-config', {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableOAuthLogin', () => {
|
||||
it('should enable oauth login', async () => {
|
||||
await sut.enableOAuthLogin();
|
||||
expect(systemMock.set).toHaveBeenCalledWith('system-config', { oauth: { enabled: true } });
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith('system-config', { oauth: { enabled: true } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,12 @@
|
|||
import {
|
||||
DatabaseExtension,
|
||||
EXTENSION_NAMES,
|
||||
IDatabaseRepository,
|
||||
VectorExtension,
|
||||
} from 'src/interfaces/database.interface';
|
||||
import { DatabaseExtension, EXTENSION_NAMES, VectorExtension } from 'src/interfaces/database.interface';
|
||||
import { DatabaseService } from 'src/services/database.service';
|
||||
import { IConfigRepository, ILoggingRepository } from 'src/types';
|
||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(DatabaseService.name, () => {
|
||||
let sut: DatabaseService;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let loggerMock: Mocked<ILoggingRepository>;
|
||||
let extensionRange: string;
|
||||
let versionBelowRange: string;
|
||||
let minVersionInRange: string;
|
||||
|
@ -23,16 +14,16 @@ describe(DatabaseService.name, () => {
|
|||
let versionAboveRange: string;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, configMock, databaseMock, loggerMock } = newTestService(DatabaseService));
|
||||
({ sut, mocks } = newTestService(DatabaseService));
|
||||
|
||||
extensionRange = '0.2.x';
|
||||
databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);
|
||||
mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange);
|
||||
|
||||
versionBelowRange = '0.1.0';
|
||||
minVersionInRange = '0.2.0';
|
||||
updateInRange = '0.2.1';
|
||||
versionAboveRange = '0.3.0';
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: minVersionInRange,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
|
@ -44,11 +35,11 @@ describe(DatabaseService.name, () => {
|
|||
|
||||
describe('onBootstrap', () => {
|
||||
it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
|
||||
databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
|
||||
mocks.database.getPostgresVersion.mockResolvedValueOnce('13.10.0');
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0');
|
||||
|
||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.getPostgresVersion).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
|
||||
|
@ -56,7 +47,7 @@ describe(DatabaseService.name, () => {
|
|||
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
|
||||
])('should work with $extensionName', ({ extension, extensionName }) => {
|
||||
beforeEach(() => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mocks.config.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
config: {
|
||||
|
@ -85,34 +76,34 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should start up successfully with ${extension}`, async () => {
|
||||
databaseMock.getPostgresVersion.mockResolvedValue('14.0.0');
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getPostgresVersion.mockResolvedValue('14.0.0');
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledWith(extension);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.getPostgresVersion).toHaveBeenCalled();
|
||||
expect(mocks.database.createExtension).toHaveBeenCalledWith(extension);
|
||||
expect(mocks.database.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if the ${extension} extension is not installed`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
|
||||
const message = `The ${extensionName} extension is not available in this Postgres instance.
|
||||
If using a container image, ensure the image has the extension installed.`;
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(message);
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.database.createExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: versionBelowRange,
|
||||
availableVersion: versionBelowRange,
|
||||
});
|
||||
|
@ -121,80 +112,80 @@ describe(DatabaseService.name, () => {
|
|||
`The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`,
|
||||
);
|
||||
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if ${extension} extension version is a nightly`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`,
|
||||
);
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.database.createExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should do in-range update for ${extension} extension`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(mocks.database.updateVectorExtension).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should not upgrade ${extension} if same version`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if ${extension} available version is below range`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: versionBelowRange,
|
||||
installedVersion: null,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.createExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if ${extension} available version is above range`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: versionAboveRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.createExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if available version is below installed version', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: updateInRange,
|
||||
});
|
||||
|
@ -203,13 +194,13 @@ describe(DatabaseService.name, () => {
|
|||
`The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`,
|
||||
);
|
||||
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if installed version is not in version range', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: versionAboveRange,
|
||||
});
|
||||
|
@ -218,84 +209,84 @@ describe(DatabaseService.name, () => {
|
|||
`The ${extensionName} extension version is ${versionAboveRange}, but Immich only supports`,
|
||||
);
|
||||
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should raise error if ${extension} extension upgrade failed`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
|
||||
mocks.database.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension');
|
||||
|
||||
expect(loggerMock.warn.mock.calls[0][0]).toContain(
|
||||
expect(mocks.logger.warn.mock.calls[0][0]).toContain(
|
||||
`The ${extensionName} extension can be updated to ${updateInRange}.`,
|
||||
);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should warn if ${extension} extension update requires restart`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true });
|
||||
mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: true });
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName);
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.warn.mock.calls[0][0]).toContain(extensionName);
|
||||
expect(mocks.database.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should reindex ${extension} indices if needed`, async () => {
|
||||
databaseMock.shouldReindex.mockResolvedValue(true);
|
||||
mocks.database.shouldReindex.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.shouldReindex).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.database.reindex).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if reindexing fails`, async () => {
|
||||
databaseMock.shouldReindex.mockResolvedValue(true);
|
||||
databaseMock.reindex.mockRejectedValue(new Error('Error reindexing'));
|
||||
mocks.database.shouldReindex.mockResolvedValue(true);
|
||||
mocks.database.reindex.mockRejectedValue(new Error('Error reindexing'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toBeDefined();
|
||||
|
||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||
expect(mocks.database.shouldReindex).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.reindex).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Could not run vector reindexing checks.'),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should not reindex ${extension} indices if not needed`, async () => {
|
||||
databaseMock.shouldReindex.mockResolvedValue(false);
|
||||
mocks.database.shouldReindex.mockResolvedValue(false);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(0);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(mocks.database.shouldReindex).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.database.reindex).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mocks.config.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
config: {
|
||||
|
@ -324,11 +315,11 @@ describe(DatabaseService.name, () => {
|
|||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if pgvector extension could not be created`, async () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mocks.config.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
config: {
|
||||
|
@ -354,41 +345,41 @@ describe(DatabaseService.name, () => {
|
|||
},
|
||||
}),
|
||||
);
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
mocks.database.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||
|
||||
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
||||
expect(mocks.logger.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal.mock.calls[0][0]).toContain(
|
||||
`Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead`,
|
||||
);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.database.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if pgvecto.rs extension could not be created`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
mocks.database.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||
|
||||
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
||||
expect(mocks.logger.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal.mock.calls[0][0]).toContain(
|
||||
`Alternatively, if your Postgres instance has pgvector, you may use this instead`,
|
||||
);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mocks.database.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -403,38 +394,38 @@ describe(DatabaseService.name, () => {
|
|||
|
||||
it('should not override interval', () => {
|
||||
sut.handleConnectionError(new Error('Error'));
|
||||
expect(loggerMock.error).toHaveBeenCalled();
|
||||
expect(mocks.logger.error).toHaveBeenCalled();
|
||||
|
||||
sut.handleConnectionError(new Error('foo'));
|
||||
expect(loggerMock.error).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should reconnect when interval elapses', async () => {
|
||||
databaseMock.reconnect.mockResolvedValue(true);
|
||||
mocks.database.reconnect.mockResolvedValue(true);
|
||||
|
||||
sut.handleConnectionError(new Error('error'));
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.log).toHaveBeenCalledWith('Database reconnected');
|
||||
expect(mocks.database.reconnect).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.log).toHaveBeenCalledWith('Database reconnected');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.reconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should try again when reconnection fails', async () => {
|
||||
databaseMock.reconnect.mockResolvedValueOnce(false);
|
||||
mocks.database.reconnect.mockResolvedValueOnce(false);
|
||||
|
||||
sut.handleConnectionError(new Error('error'));
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.warn).toHaveBeenCalledWith(expect.stringContaining('Database connection failed'));
|
||||
expect(mocks.database.reconnect).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.warn).toHaveBeenCalledWith(expect.stringContaining('Database connection failed'));
|
||||
|
||||
databaseMock.reconnect.mockResolvedValueOnce(true);
|
||||
mocks.database.reconnect.mockResolvedValueOnce(true);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(databaseMock.reconnect).toHaveBeenCalledTimes(2);
|
||||
expect(loggerMock.log).toHaveBeenCalledWith('Database reconnected');
|
||||
expect(mocks.database.reconnect).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.logger.log).toHaveBeenCalledWith('Database reconnected');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { DownloadResponseDto } from 'src/dtos/download.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { DownloadService } from 'src/services/download.service';
|
||||
import { ILoggingRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { Readable } from 'typeorm/platform/PlatformTools.js';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
import { vitest } from 'vitest';
|
||||
|
||||
const downloadResponse: DownloadResponseDto = {
|
||||
totalSize: 105_000,
|
||||
|
@ -24,17 +20,14 @@ const downloadResponse: DownloadResponseDto = {
|
|||
|
||||
describe(DownloadService.name, () => {
|
||||
let sut: DownloadService;
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let loggerMock: Mocked<ILoggingRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, assetMock, loggerMock, storageMock } = newTestService(DownloadService));
|
||||
({ sut, mocks } = newTestService(DownloadService));
|
||||
});
|
||||
|
||||
describe('downloadArchive', () => {
|
||||
|
@ -45,9 +38,9 @@ describe(DownloadService.name, () => {
|
|||
stream: new Readable(),
|
||||
};
|
||||
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.noResizePath, id: 'asset-1' }]);
|
||||
storageMock.createZipStream.mockReturnValue(archiveMock);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.noResizePath, id: 'asset-1' }]);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
|
@ -64,19 +57,19 @@ describe(DownloadService.name, () => {
|
|||
stream: new Readable(),
|
||||
};
|
||||
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
storageMock.realpath.mockRejectedValue(new Error('Could not read file'));
|
||||
assetMock.getByIds.mockResolvedValue([
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.storage.realpath.mockRejectedValue(new Error('Could not read file'));
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{ ...assetStub.noResizePath, id: 'asset-1' },
|
||||
{ ...assetStub.noWebpPath, id: 'asset-2' },
|
||||
]);
|
||||
storageMock.createZipStream.mockReturnValue(archiveMock);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
});
|
||||
|
||||
expect(loggerMock.warn).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.logger.warn).toHaveBeenCalledTimes(2);
|
||||
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg');
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_456.jpg', 'IMG_456.jpg');
|
||||
|
@ -89,12 +82,12 @@ describe(DownloadService.name, () => {
|
|||
stream: new Readable(),
|
||||
};
|
||||
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
assetMock.getByIds.mockResolvedValue([
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{ ...assetStub.noResizePath, id: 'asset-1' },
|
||||
{ ...assetStub.noWebpPath, id: 'asset-2' },
|
||||
]);
|
||||
storageMock.createZipStream.mockReturnValue(archiveMock);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
|
@ -112,12 +105,12 @@ describe(DownloadService.name, () => {
|
|||
stream: new Readable(),
|
||||
};
|
||||
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
assetMock.getByIds.mockResolvedValue([
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{ ...assetStub.noResizePath, id: 'asset-1' },
|
||||
{ ...assetStub.noResizePath, id: 'asset-2' },
|
||||
]);
|
||||
storageMock.createZipStream.mockReturnValue(archiveMock);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
|
@ -135,12 +128,12 @@ describe(DownloadService.name, () => {
|
|||
stream: new Readable(),
|
||||
};
|
||||
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
assetMock.getByIds.mockResolvedValue([
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{ ...assetStub.noResizePath, id: 'asset-2' },
|
||||
{ ...assetStub.noResizePath, id: 'asset-1' },
|
||||
]);
|
||||
storageMock.createZipStream.mockReturnValue(archiveMock);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
|
@ -158,12 +151,12 @@ describe(DownloadService.name, () => {
|
|||
stream: new Readable(),
|
||||
};
|
||||
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
assetMock.getByIds.mockResolvedValue([
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{ ...assetStub.noResizePath, id: 'asset-1', originalPath: '/path/to/symlink.jpg' },
|
||||
]);
|
||||
storageMock.realpath.mockResolvedValue('/path/to/realpath.jpg');
|
||||
storageMock.createZipStream.mockReturnValue(archiveMock);
|
||||
mocks.storage.realpath.mockResolvedValue('/path/to/realpath.jpg');
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1'] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
|
@ -179,30 +172,30 @@ describe(DownloadService.name, () => {
|
|||
});
|
||||
|
||||
it('should return a list of archives (assetIds)', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image, assetStub.video]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image, assetStub.video]);
|
||||
|
||||
const assetIds = ['asset-1', 'asset-2'];
|
||||
await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual(downloadResponse);
|
||||
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith(['asset-1', 'asset-2'], { exifInfo: true });
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith(['asset-1', 'asset-2'], { exifInfo: true });
|
||||
});
|
||||
|
||||
it('should return a list of archives (albumId)', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
|
||||
assetMock.getByAlbumId.mockResolvedValue({
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
|
||||
mocks.asset.getByAlbumId.mockResolvedValue({
|
||||
items: [assetStub.image, assetStub.video],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
await expect(sut.getDownloadInfo(authStub.admin, { albumId: 'album-1' })).resolves.toEqual(downloadResponse);
|
||||
|
||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-1']));
|
||||
expect(assetMock.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1');
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-1']));
|
||||
expect(mocks.asset.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1');
|
||||
});
|
||||
|
||||
it('should return a list of archives (userId)', async () => {
|
||||
assetMock.getByUserId.mockResolvedValue({
|
||||
mocks.asset.getByUserId.mockResolvedValue({
|
||||
items: [assetStub.image, assetStub.video],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
@ -211,13 +204,13 @@ describe(DownloadService.name, () => {
|
|||
downloadResponse,
|
||||
);
|
||||
|
||||
expect(assetMock.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.user.id, {
|
||||
expect(mocks.asset.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.user.id, {
|
||||
isVisible: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should split archives by size', async () => {
|
||||
assetMock.getByUserId.mockResolvedValue({
|
||||
mocks.asset.getByUserId.mockResolvedValue({
|
||||
items: [
|
||||
{ ...assetStub.image, id: 'asset-1' },
|
||||
{ ...assetStub.video, id: 'asset-2' },
|
||||
|
@ -245,8 +238,8 @@ describe(DownloadService.name, () => {
|
|||
const assetIds = [assetStub.livePhotoStillAsset.id];
|
||||
const assets = [assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset];
|
||||
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
|
||||
assetMock.getByIds.mockImplementation(
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
|
||||
mocks.asset.getByIds.mockImplementation(
|
||||
(ids) =>
|
||||
Promise.resolve(
|
||||
ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset),
|
||||
|
@ -271,8 +264,8 @@ describe(DownloadService.name, () => {
|
|||
{ ...assetStub.livePhotoMotionAsset, originalPath: 'upload/encoded-video/uuid-MP.mp4' },
|
||||
];
|
||||
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
|
||||
assetMock.getByIds.mockImplementation(
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
|
||||
mocks.asset.getByIds.mockImplementation(
|
||||
(ids) =>
|
||||
Promise.resolve(
|
||||
ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset),
|
||||
|
|
|
@ -1,27 +1,20 @@
|
|||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { ILoggingRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked, beforeEach, vitest } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { beforeEach, vitest } from 'vitest';
|
||||
|
||||
vitest.useFakeTimers();
|
||||
|
||||
describe(SearchService.name, () => {
|
||||
let sut: DuplicateService;
|
||||
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let loggerMock: Mocked<ILoggingRepository>;
|
||||
let searchMock: Mocked<ISearchRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, assetMock, jobMock, loggerMock, searchMock, systemMock } = newTestService(DuplicateService));
|
||||
({ sut, mocks } = newTestService(DuplicateService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -30,7 +23,7 @@ describe(SearchService.name, () => {
|
|||
|
||||
describe('getDuplicates', () => {
|
||||
it('should get duplicates', async () => {
|
||||
assetMock.getDuplicates.mockResolvedValue([
|
||||
mocks.asset.getDuplicates.mockResolvedValue([
|
||||
{
|
||||
duplicateId: assetStub.hasDupe.duplicateId!,
|
||||
assets: [assetStub.hasDupe, assetStub.hasDupe],
|
||||
|
@ -50,7 +43,7 @@ describe(SearchService.name, () => {
|
|||
|
||||
describe('handleQueueSearchDuplicates', () => {
|
||||
beforeEach(() => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
machineLearning: {
|
||||
enabled: true,
|
||||
duplicateDetection: {
|
||||
|
@ -61,7 +54,7 @@ describe(SearchService.name, () => {
|
|||
});
|
||||
|
||||
it('should skip if machine learning is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
machineLearning: {
|
||||
enabled: false,
|
||||
duplicateDetection: {
|
||||
|
@ -71,13 +64,13 @@ describe(SearchService.name, () => {
|
|||
});
|
||||
|
||||
await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if duplicate detection is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
machineLearning: {
|
||||
enabled: true,
|
||||
duplicateDetection: {
|
||||
|
@ -87,21 +80,21 @@ describe(SearchService.name, () => {
|
|||
});
|
||||
|
||||
await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue missing assets', async () => {
|
||||
assetMock.getWithout.mockResolvedValue({
|
||||
mocks.asset.getWithout.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
await sut.handleQueueSearchDuplicates({});
|
||||
|
||||
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.DUPLICATE);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
expect(mocks.asset.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.DUPLICATE);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.DUPLICATE_DETECTION,
|
||||
data: { id: assetStub.image.id },
|
||||
|
@ -110,15 +103,15 @@ describe(SearchService.name, () => {
|
|||
});
|
||||
|
||||
it('should queue all assets', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
await sut.handleQueueSearchDuplicates({ force: true });
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.DUPLICATE_DETECTION,
|
||||
data: { id: assetStub.image.id },
|
||||
|
@ -129,7 +122,7 @@ describe(SearchService.name, () => {
|
|||
|
||||
describe('handleSearchDuplicates', () => {
|
||||
beforeEach(() => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
machineLearning: {
|
||||
enabled: true,
|
||||
duplicateDetection: {
|
||||
|
@ -140,7 +133,7 @@ describe(SearchService.name, () => {
|
|||
});
|
||||
|
||||
it('should skip if machine learning is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
machineLearning: {
|
||||
enabled: false,
|
||||
duplicateDetection: {
|
||||
|
@ -149,7 +142,7 @@ describe(SearchService.name, () => {
|
|||
},
|
||||
});
|
||||
const id = assetStub.livePhotoMotionAsset.id;
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id });
|
||||
|
||||
|
@ -157,7 +150,7 @@ describe(SearchService.name, () => {
|
|||
});
|
||||
|
||||
it('should skip if duplicate detection is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
machineLearning: {
|
||||
enabled: true,
|
||||
duplicateDetection: {
|
||||
|
@ -166,7 +159,7 @@ describe(SearchService.name, () => {
|
|||
},
|
||||
});
|
||||
const id = assetStub.livePhotoMotionAsset.id;
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id });
|
||||
|
||||
|
@ -177,40 +170,40 @@ describe(SearchService.name, () => {
|
|||
const result = await sut.handleSearchDuplicates({ id: assetStub.image.id });
|
||||
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
expect(loggerMock.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`);
|
||||
expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`);
|
||||
});
|
||||
|
||||
it('should skip if asset is not visible', async () => {
|
||||
const id = assetStub.livePhotoMotionAsset.id;
|
||||
assetMock.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id });
|
||||
|
||||
expect(result).toBe(JobStatus.SKIPPED);
|
||||
expect(loggerMock.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`);
|
||||
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`);
|
||||
});
|
||||
|
||||
it('should fail if asset is missing preview image', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.noResizePath);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.noResizePath);
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id: assetStub.noResizePath.id });
|
||||
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
expect(loggerMock.warn).toHaveBeenCalledWith(`Asset ${assetStub.noResizePath.id} is missing preview image`);
|
||||
expect(mocks.logger.warn).toHaveBeenCalledWith(`Asset ${assetStub.noResizePath.id} is missing preview image`);
|
||||
});
|
||||
|
||||
it('should fail if asset is missing embedding', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id: assetStub.image.id });
|
||||
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
expect(loggerMock.debug).toHaveBeenCalledWith(`Asset ${assetStub.image.id} is missing embedding`);
|
||||
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${assetStub.image.id} is missing embedding`);
|
||||
});
|
||||
|
||||
it('should search for duplicates and update asset with duplicateId', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.hasEmbedding);
|
||||
searchMock.searchDuplicates.mockResolvedValue([
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.hasEmbedding);
|
||||
mocks.search.searchDuplicates.mockResolvedValue([
|
||||
{ assetId: assetStub.image.id, distance: 0.01, duplicateId: null },
|
||||
]);
|
||||
const expectedAssetIds = [assetStub.image.id, assetStub.hasEmbedding.id];
|
||||
|
@ -218,58 +211,58 @@ describe(SearchService.name, () => {
|
|||
const result = await sut.handleSearchDuplicates({ id: assetStub.hasEmbedding.id });
|
||||
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(searchMock.searchDuplicates).toHaveBeenCalledWith({
|
||||
expect(mocks.search.searchDuplicates).toHaveBeenCalledWith({
|
||||
assetId: assetStub.hasEmbedding.id,
|
||||
embedding: assetStub.hasEmbedding.smartSearch!.embedding,
|
||||
maxDistance: 0.01,
|
||||
type: assetStub.hasEmbedding.type,
|
||||
userIds: [assetStub.hasEmbedding.ownerId],
|
||||
});
|
||||
expect(assetMock.updateDuplicates).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.updateDuplicates).toHaveBeenCalledWith({
|
||||
assetIds: expectedAssetIds,
|
||||
targetDuplicateId: expect.any(String),
|
||||
duplicateIds: [],
|
||||
});
|
||||
expect(assetMock.upsertJobStatus).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith(
|
||||
...expectedAssetIds.map((assetId) => ({ assetId, duplicatesDetectedAt: expect.any(Date) })),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use existing duplicate ID among matched duplicates', async () => {
|
||||
const duplicateId = assetStub.hasDupe.duplicateId;
|
||||
assetMock.getById.mockResolvedValue(assetStub.hasEmbedding);
|
||||
searchMock.searchDuplicates.mockResolvedValue([{ assetId: assetStub.hasDupe.id, distance: 0.01, duplicateId }]);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.hasEmbedding);
|
||||
mocks.search.searchDuplicates.mockResolvedValue([{ assetId: assetStub.hasDupe.id, distance: 0.01, duplicateId }]);
|
||||
const expectedAssetIds = [assetStub.hasEmbedding.id];
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id: assetStub.hasEmbedding.id });
|
||||
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(searchMock.searchDuplicates).toHaveBeenCalledWith({
|
||||
expect(mocks.search.searchDuplicates).toHaveBeenCalledWith({
|
||||
assetId: assetStub.hasEmbedding.id,
|
||||
embedding: assetStub.hasEmbedding.smartSearch!.embedding,
|
||||
maxDistance: 0.01,
|
||||
type: assetStub.hasEmbedding.type,
|
||||
userIds: [assetStub.hasEmbedding.ownerId],
|
||||
});
|
||||
expect(assetMock.updateDuplicates).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.updateDuplicates).toHaveBeenCalledWith({
|
||||
assetIds: expectedAssetIds,
|
||||
targetDuplicateId: assetStub.hasDupe.duplicateId,
|
||||
duplicateIds: [],
|
||||
});
|
||||
expect(assetMock.upsertJobStatus).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith(
|
||||
...expectedAssetIds.map((assetId) => ({ assetId, duplicatesDetectedAt: expect.any(Date) })),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove duplicateId if no duplicates found and asset has duplicateId', async () => {
|
||||
assetMock.getById.mockResolvedValue(assetStub.hasDupe);
|
||||
searchMock.searchDuplicates.mockResolvedValue([]);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.hasDupe);
|
||||
mocks.search.searchDuplicates.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id: assetStub.hasDupe.id });
|
||||
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.hasDupe.id, duplicateId: null });
|
||||
expect(assetMock.upsertJobStatus).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.hasDupe.id, duplicateId: null });
|
||||
expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith({
|
||||
assetId: assetStub.hasDupe.id,
|
||||
duplicatesDetectedAt: expect.any(Date),
|
||||
});
|
||||
|
|
|
@ -1,27 +1,19 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
|
||||
import { JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { IConfigRepository, ILoggingRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { ITelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(JobService.name, () => {
|
||||
let sut: JobService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let loggerMock: Mocked<ILoggingRepository>;
|
||||
let telemetryMock: ITelemetryRepositoryMock;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, assetMock, configMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {}));
|
||||
({ sut, mocks } = newTestService(JobService, {}));
|
||||
|
||||
configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||
mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -32,11 +24,11 @@ describe(JobService.name, () => {
|
|||
it('should update concurrency', () => {
|
||||
sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig });
|
||||
|
||||
expect(jobMock.setConcurrency).toHaveBeenCalledTimes(15);
|
||||
expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FACIAL_RECOGNITION, 1);
|
||||
expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DUPLICATE_DETECTION, 1);
|
||||
expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BACKGROUND_TASK, 5);
|
||||
expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.STORAGE_TEMPLATE_MIGRATION, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(15);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FACIAL_RECOGNITION, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DUPLICATE_DETECTION, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BACKGROUND_TASK, 5);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.STORAGE_TEMPLATE_MIGRATION, 1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -44,7 +36,7 @@ describe(JobService.name, () => {
|
|||
it('should run the scheduled jobs', async () => {
|
||||
await sut.handleNightlyJobs();
|
||||
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.ASSET_DELETION_CHECK },
|
||||
{ name: JobName.USER_DELETE_CHECK },
|
||||
{ name: JobName.PERSON_CLEANUP },
|
||||
|
@ -59,7 +51,7 @@ describe(JobService.name, () => {
|
|||
|
||||
describe('getAllJobStatus', () => {
|
||||
it('should get all job statuses', async () => {
|
||||
jobMock.getJobCounts.mockResolvedValue({
|
||||
mocks.job.getJobCounts.mockResolvedValue({
|
||||
active: 1,
|
||||
completed: 1,
|
||||
failed: 1,
|
||||
|
@ -67,7 +59,7 @@ describe(JobService.name, () => {
|
|||
waiting: 1,
|
||||
paused: 1,
|
||||
});
|
||||
jobMock.getQueueStatus.mockResolvedValue({
|
||||
mocks.job.getQueueStatus.mockResolvedValue({
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
});
|
||||
|
@ -111,121 +103,121 @@ describe(JobService.name, () => {
|
|||
it('should handle a pause command', async () => {
|
||||
await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.PAUSE, force: false });
|
||||
|
||||
expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||
});
|
||||
|
||||
it('should handle a resume command', async () => {
|
||||
await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.RESUME, force: false });
|
||||
|
||||
expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||
});
|
||||
|
||||
it('should handle an empty command', async () => {
|
||||
await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.EMPTY, force: false });
|
||||
|
||||
expect(jobMock.empty).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||
});
|
||||
|
||||
it('should not start a job that is already running', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false });
|
||||
|
||||
await expect(
|
||||
sut.handleCommand(QueueName.VIDEO_CONVERSION, { command: JobCommand.START, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a start video conversion command', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.VIDEO_CONVERSION, { command: JobCommand.START, force: false });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force: false } });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start storage template migration command', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.STORAGE_TEMPLATE_MIGRATION, { command: JobCommand.START, force: false });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
|
||||
});
|
||||
|
||||
it('should handle a start smart search command', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.SMART_SEARCH, { command: JobCommand.START, force: false });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SMART_SEARCH, data: { force: false } });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SMART_SEARCH, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start metadata extraction command', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.START, force: false });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force: false } });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start sidecar command', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.SIDECAR, { command: JobCommand.START, force: false });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SIDECAR, data: { force: false } });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SIDECAR, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start thumbnail generation command', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.THUMBNAIL_GENERATION, { command: JobCommand.START, force: false });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start face detection command', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.FACE_DETECTION, { command: JobCommand.START, force: false });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_FACE_DETECTION, data: { force: false } });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_FACE_DETECTION, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start facial recognition command', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.FACIAL_RECOGNITION, { command: JobCommand.START, force: false });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should throw a bad request when an invalid queue is used', async () => {
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await expect(
|
||||
sut.handleCommand(QueueName.BACKGROUND_TASK, { command: JobCommand.START, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onJobStart', () => {
|
||||
it('should process a successful job', async () => {
|
||||
jobMock.run.mockResolvedValue(JobStatus.SUCCESS);
|
||||
mocks.job.run.mockResolvedValue(JobStatus.SUCCESS);
|
||||
|
||||
await sut.onJobStart(QueueName.BACKGROUND_TASK, {
|
||||
name: JobName.DELETE_FILES,
|
||||
data: { files: ['path/to/file'] },
|
||||
});
|
||||
|
||||
expect(telemetryMock.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', 1);
|
||||
expect(telemetryMock.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', -1);
|
||||
expect(telemetryMock.jobs.addToCounter).toHaveBeenCalledWith('immich.jobs.delete_files.success', 1);
|
||||
expect(loggerMock.error).not.toHaveBeenCalled();
|
||||
expect(mocks.telemetry.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', 1);
|
||||
expect(mocks.telemetry.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', -1);
|
||||
expect(mocks.telemetry.jobs.addToCounter).toHaveBeenCalledWith('immich.jobs.delete_files.success', 1);
|
||||
expect(mocks.logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const tests: Array<{ item: JobItem; jobs: JobName[] }> = [
|
||||
|
@ -287,34 +279,34 @@ describe(JobService.name, () => {
|
|||
it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => {
|
||||
if (item.name === JobName.GENERATE_THUMBNAILS && item.data.source === 'upload') {
|
||||
if (item.data.id === 'asset-live-image') {
|
||||
assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
||||
mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
||||
} else {
|
||||
assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
||||
mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
||||
}
|
||||
}
|
||||
|
||||
jobMock.run.mockResolvedValue(JobStatus.SUCCESS);
|
||||
mocks.job.run.mockResolvedValue(JobStatus.SUCCESS);
|
||||
|
||||
await sut.onJobStart(QueueName.BACKGROUND_TASK, item);
|
||||
|
||||
if (jobs.length > 1) {
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith(
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith(
|
||||
jobs.map((jobName) => ({ name: jobName, data: expect.anything() })),
|
||||
);
|
||||
} else {
|
||||
expect(jobMock.queue).toHaveBeenCalledTimes(jobs.length);
|
||||
expect(mocks.job.queue).toHaveBeenCalledTimes(jobs.length);
|
||||
for (const jobName of jobs) {
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: jobName, data: expect.anything() });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: jobName, data: expect.anything() });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it(`should not queue any jobs when ${item.name} fails`, async () => {
|
||||
jobMock.run.mockResolvedValue(JobStatus.FAILED);
|
||||
mocks.job.run.mockResolvedValue(JobStatus.FAILED);
|
||||
|
||||
await sut.onJobStart(QueueName.BACKGROUND_TASK, item);
|
||||
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,23 +1,16 @@
|
|||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { MapService } from 'src/services/map.service';
|
||||
import { IMapRepository } from 'src/types';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(MapService.name, () => {
|
||||
let sut: MapService;
|
||||
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let mapMock: Mocked<IMapRepository>;
|
||||
let partnerMock: Mocked<IPartnerRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, albumMock, mapMock, partnerMock } = newTestService(MapService));
|
||||
({ sut, mocks } = newTestService(MapService));
|
||||
});
|
||||
|
||||
describe('getMapMarkers', () => {
|
||||
|
@ -31,8 +24,8 @@ describe(MapService.name, () => {
|
|||
state: asset.exifInfo!.state,
|
||||
country: asset.exifInfo!.country,
|
||||
};
|
||||
partnerMock.getAll.mockResolvedValue([]);
|
||||
mapMock.getMapMarkers.mockResolvedValue([marker]);
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||
|
||||
const markers = await sut.getMapMarkers(authStub.user1, {});
|
||||
|
||||
|
@ -50,12 +43,12 @@ describe(MapService.name, () => {
|
|||
state: asset.exifInfo!.state,
|
||||
country: asset.exifInfo!.country,
|
||||
};
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]);
|
||||
mapMock.getMapMarkers.mockResolvedValue([marker]);
|
||||
mocks.partner.getAll.mockResolvedValue([partnerStub.adminToUser1]);
|
||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||
|
||||
const markers = await sut.getMapMarkers(authStub.user1, { withPartners: true });
|
||||
|
||||
expect(mapMock.getMapMarkers).toHaveBeenCalledWith(
|
||||
expect(mocks.map.getMapMarkers).toHaveBeenCalledWith(
|
||||
[authStub.user1.user.id, partnerStub.adminToUser1.sharedById],
|
||||
expect.arrayContaining([]),
|
||||
{ withPartners: true },
|
||||
|
@ -74,10 +67,10 @@ describe(MapService.name, () => {
|
|||
state: asset.exifInfo!.state,
|
||||
country: asset.exifInfo!.country,
|
||||
};
|
||||
partnerMock.getAll.mockResolvedValue([]);
|
||||
mapMock.getMapMarkers.mockResolvedValue([marker]);
|
||||
albumMock.getOwned.mockResolvedValue([albumStub.empty]);
|
||||
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||
mocks.album.getOwned.mockResolvedValue([albumStub.empty]);
|
||||
mocks.album.getShared.mockResolvedValue([albumStub.sharedWithUser]);
|
||||
|
||||
const markers = await sut.getMapMarkers(authStub.user1, { withSharedAlbums: true });
|
||||
|
||||
|
@ -88,13 +81,13 @@ describe(MapService.name, () => {
|
|||
|
||||
describe('reverseGeocode', () => {
|
||||
it('should reverse geocode a location', async () => {
|
||||
mapMock.reverseGeocode.mockResolvedValue({ city: 'foo', state: 'bar', country: 'baz' });
|
||||
mocks.map.reverseGeocode.mockResolvedValue({ city: 'foo', state: 'bar', country: 'baz' });
|
||||
|
||||
await expect(sut.reverseGeocode({ lat: 42, lon: 69 })).resolves.toEqual([
|
||||
{ city: 'foo', state: 'bar', country: 'baz' },
|
||||
]);
|
||||
|
||||
expect(mapMock.reverseGeocode).toHaveBeenCalledWith({ latitude: 42, longitude: 69 });
|
||||
expect(mocks.map.reverseGeocode).toHaveBeenCalledWith({ latitude: 42, longitude: 69 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,22 +1,17 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { MemoryType } from 'src/enum';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { IMemoryRepository } from 'src/types';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { memoryStub } from 'test/fixtures/memory.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(MemoryService.name, () => {
|
||||
let sut: MemoryService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let memoryMock: Mocked<IMemoryRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, memoryMock } = newTestService(MemoryService));
|
||||
({ sut, mocks } = newTestService(MemoryService));
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
@ -25,7 +20,7 @@ describe(MemoryService.name, () => {
|
|||
|
||||
describe('search', () => {
|
||||
it('should search memories', async () => {
|
||||
memoryMock.search.mockResolvedValue([memoryStub.memory1, memoryStub.empty]);
|
||||
mocks.memory.search.mockResolvedValue([memoryStub.memory1, memoryStub.empty]);
|
||||
await expect(sut.search(authStub.admin)).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'memory1', assets: expect.any(Array) }),
|
||||
|
@ -45,22 +40,22 @@ describe(MemoryService.name, () => {
|
|||
});
|
||||
|
||||
it('should throw an error when the memory is not found', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['race-condition']));
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['race-condition']));
|
||||
await expect(sut.get(authStub.admin, 'race-condition')).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should get a memory by id', async () => {
|
||||
memoryMock.get.mockResolvedValue(memoryStub.memory1);
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
mocks.memory.get.mockResolvedValue(memoryStub.memory1);
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
await expect(sut.get(authStub.admin, 'memory1')).resolves.toMatchObject({ id: 'memory1' });
|
||||
expect(memoryMock.get).toHaveBeenCalledWith('memory1');
|
||||
expect(accessMock.memory.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['memory1']));
|
||||
expect(mocks.memory.get).toHaveBeenCalledWith('memory1');
|
||||
expect(mocks.access.memory.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['memory1']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should skip assets the user does not have access to', async () => {
|
||||
memoryMock.create.mockResolvedValue(memoryStub.empty);
|
||||
mocks.memory.create.mockResolvedValue(memoryStub.empty);
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
|
@ -69,7 +64,7 @@ describe(MemoryService.name, () => {
|
|||
memoryAt: new Date(2024),
|
||||
}),
|
||||
).resolves.toMatchObject({ assets: [] });
|
||||
expect(memoryMock.create).toHaveBeenCalledWith(
|
||||
expect(mocks.memory.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: 'admin_id',
|
||||
memoryAt: expect.any(Date),
|
||||
|
@ -83,8 +78,8 @@ describe(MemoryService.name, () => {
|
|||
});
|
||||
|
||||
it('should create a memory', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
memoryMock.create.mockResolvedValue(memoryStub.memory1);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
mocks.memory.create.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
|
@ -93,7 +88,7 @@ describe(MemoryService.name, () => {
|
|||
memoryAt: new Date(2024, 0, 1),
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
expect(memoryMock.create).toHaveBeenCalledWith(
|
||||
expect(mocks.memory.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ownerId: userStub.admin.id,
|
||||
}),
|
||||
|
@ -102,7 +97,7 @@ describe(MemoryService.name, () => {
|
|||
});
|
||||
|
||||
it('should create a memory without assets', async () => {
|
||||
memoryMock.create.mockResolvedValue(memoryStub.memory1);
|
||||
mocks.memory.create.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
|
@ -118,27 +113,27 @@ describe(MemoryService.name, () => {
|
|||
await expect(sut.update(authStub.admin, 'not-found', { isSaved: true })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(memoryMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.memory.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update a memory', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
memoryMock.update.mockResolvedValue(memoryStub.memory1);
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
mocks.memory.update.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined();
|
||||
expect(memoryMock.update).toHaveBeenCalledWith('memory1', expect.objectContaining({ isSaved: true }));
|
||||
expect(mocks.memory.update).toHaveBeenCalledWith('memory1', expect.objectContaining({ isSaved: true }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should require access', async () => {
|
||||
await expect(sut.remove(authStub.admin, 'not-found')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(memoryMock.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.memory.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete a memory', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
await expect(sut.remove(authStub.admin, 'memory1')).resolves.toBeUndefined();
|
||||
expect(memoryMock.delete).toHaveBeenCalledWith('memory1');
|
||||
expect(mocks.memory.delete).toHaveBeenCalledWith('memory1');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -147,36 +142,36 @@ describe(MemoryService.name, () => {
|
|||
await expect(sut.addAssets(authStub.admin, 'not-found', { ids: ['asset1'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(memoryMock.addAssetIds).not.toHaveBeenCalled();
|
||||
expect(mocks.memory.addAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require asset access', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
memoryMock.get.mockResolvedValue(memoryStub.memory1);
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
mocks.memory.get.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['not-found'] })).resolves.toEqual([
|
||||
{ error: 'no_permission', id: 'not-found', success: false },
|
||||
]);
|
||||
expect(memoryMock.addAssetIds).not.toHaveBeenCalled();
|
||||
expect(mocks.memory.addAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip assets already in the memory', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
memoryMock.get.mockResolvedValue(memoryStub.memory1);
|
||||
memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1']));
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
mocks.memory.get.mockResolvedValue(memoryStub.memory1);
|
||||
mocks.memory.getAssetIds.mockResolvedValue(new Set(['asset1']));
|
||||
await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([
|
||||
{ error: 'duplicate', id: 'asset1', success: false },
|
||||
]);
|
||||
expect(memoryMock.addAssetIds).not.toHaveBeenCalled();
|
||||
expect(mocks.memory.addAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add assets', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
memoryMock.get.mockResolvedValue(memoryStub.memory1);
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
mocks.memory.get.mockResolvedValue(memoryStub.memory1);
|
||||
await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([
|
||||
{ id: 'asset1', success: true },
|
||||
]);
|
||||
expect(memoryMock.addAssetIds).toHaveBeenCalledWith('memory1', ['asset1']);
|
||||
expect(mocks.memory.addAssetIds).toHaveBeenCalledWith('memory1', ['asset1']);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -185,25 +180,25 @@ describe(MemoryService.name, () => {
|
|||
await expect(sut.removeAssets(authStub.admin, 'not-found', { ids: ['asset1'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(memoryMock.removeAssetIds).not.toHaveBeenCalled();
|
||||
expect(mocks.memory.removeAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip assets not in the memory', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['not-found'] })).resolves.toEqual([
|
||||
{ error: 'not_found', id: 'not-found', success: false },
|
||||
]);
|
||||
expect(memoryMock.removeAssetIds).not.toHaveBeenCalled();
|
||||
expect(mocks.memory.removeAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove assets', async () => {
|
||||
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1']));
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
|
||||
mocks.memory.getAssetIds.mockResolvedValue(new Set(['asset1']));
|
||||
await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([
|
||||
{ id: 'asset1', success: true },
|
||||
]);
|
||||
expect(memoryMock.removeAssetIds).toHaveBeenCalledWith('memory1', ['asset1']);
|
||||
expect(mocks.memory.removeAssetIds).toHaveBeenCalledWith('memory1', ['asset1']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,19 +4,13 @@ import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
|||
import { AlbumUserEntity } from 'src/entities/album-user.entity';
|
||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||
import { AssetFileType, UserMetadataKey } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, INotifyAlbumUpdateJob, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { INotifyAlbumUpdateJob, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { EmailTemplate } from 'src/repositories/notification.repository';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { INotificationRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const configs = {
|
||||
smtpDisabled: Object.freeze<SystemConfig>({
|
||||
|
@ -57,18 +51,10 @@ const configs = {
|
|||
|
||||
describe(NotificationService.name, () => {
|
||||
let sut: NotificationService;
|
||||
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let notificationMock: Mocked<INotificationRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, albumMock, assetMock, eventMock, jobMock, notificationMock, systemMock, userMock } =
|
||||
newTestService(NotificationService));
|
||||
({ sut, mocks } = newTestService(NotificationService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -79,8 +65,8 @@ describe(NotificationService.name, () => {
|
|||
it('should emit client and server events', () => {
|
||||
const update = { oldConfig: defaults, newConfig: defaults };
|
||||
expect(sut.onConfigUpdate(update)).toBeUndefined();
|
||||
expect(eventMock.clientBroadcast).toHaveBeenCalledWith('on_config_update');
|
||||
expect(eventMock.serverSend).toHaveBeenCalledWith('config.update', update);
|
||||
expect(mocks.event.clientBroadcast).toHaveBeenCalledWith('on_config_update');
|
||||
expect(mocks.event.serverSend).toHaveBeenCalledWith('config.update', update);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -89,18 +75,18 @@ describe(NotificationService.name, () => {
|
|||
const oldConfig = configs.smtpDisabled;
|
||||
const newConfig = configs.smtpEnabled;
|
||||
|
||||
notificationMock.verifySmtp.mockResolvedValue(true);
|
||||
mocks.notification.verifySmtp.mockResolvedValue(true);
|
||||
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||
expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
|
||||
expect(mocks.notification.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
|
||||
});
|
||||
|
||||
it('validates smtp config when transport changes', async () => {
|
||||
const oldConfig = configs.smtpEnabled;
|
||||
const newConfig = configs.smtpTransport;
|
||||
|
||||
notificationMock.verifySmtp.mockResolvedValue(true);
|
||||
mocks.notification.verifySmtp.mockResolvedValue(true);
|
||||
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||
expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
|
||||
expect(mocks.notification.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport);
|
||||
});
|
||||
|
||||
it('skips smtp validation when there are no changes', async () => {
|
||||
|
@ -108,7 +94,7 @@ describe(NotificationService.name, () => {
|
|||
const newConfig = { ...configs.smtpEnabled };
|
||||
|
||||
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
|
||||
expect(mocks.notification.verifySmtp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips smtp validation with DTO when there are no changes', async () => {
|
||||
|
@ -116,7 +102,7 @@ describe(NotificationService.name, () => {
|
|||
const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled);
|
||||
|
||||
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
|
||||
expect(mocks.notification.verifySmtp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips smtp validation when smtp is disabled', async () => {
|
||||
|
@ -124,14 +110,14 @@ describe(NotificationService.name, () => {
|
|||
const newConfig = { ...configs.smtpDisabled };
|
||||
|
||||
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||
expect(notificationMock.verifySmtp).not.toHaveBeenCalled();
|
||||
expect(mocks.notification.verifySmtp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if smtp configuration is invalid', async () => {
|
||||
const oldConfig = configs.smtpDisabled;
|
||||
const newConfig = configs.smtpEnabled;
|
||||
|
||||
notificationMock.verifySmtp.mockRejectedValue(new Error('Failed validating smtp'));
|
||||
mocks.notification.verifySmtp.mockRejectedValue(new Error('Failed validating smtp'));
|
||||
await expect(sut.onConfigValidate({ oldConfig, newConfig })).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
@ -139,14 +125,14 @@ describe(NotificationService.name, () => {
|
|||
describe('onAssetHide', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onAssetHide({ assetId: 'asset-id', userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_hidden', 'user-id', 'asset-id');
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_hidden', 'user-id', 'asset-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAssetShow', () => {
|
||||
it('should queue the generate thumbnail job', async () => {
|
||||
await sut.onAssetShow({ assetId: 'asset-id', userId: 'user-id' });
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.GENERATE_THUMBNAILS,
|
||||
data: { id: 'asset-id', notify: true },
|
||||
});
|
||||
|
@ -156,12 +142,12 @@ describe(NotificationService.name, () => {
|
|||
describe('onUserSignupEvent', () => {
|
||||
it('skips when notify is false', async () => {
|
||||
await sut.onUserSignup({ id: '', notify: false });
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue notify signup event if notify is true', async () => {
|
||||
await sut.onUserSignup({ id: '', notify: true });
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.NOTIFY_SIGNUP,
|
||||
data: { id: '', tempPassword: undefined },
|
||||
});
|
||||
|
@ -171,7 +157,7 @@ describe(NotificationService.name, () => {
|
|||
describe('onAlbumUpdateEvent', () => {
|
||||
it('should queue notify album update event', async () => {
|
||||
await sut.onAlbumUpdate({ id: 'album', recipientIds: ['42'] });
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
data: { id: 'album', recipientIds: ['42'], delay: 300_000 },
|
||||
});
|
||||
|
@ -181,7 +167,7 @@ describe(NotificationService.name, () => {
|
|||
describe('onAlbumInviteEvent', () => {
|
||||
it('should queue notify album invite event', async () => {
|
||||
await sut.onAlbumInvite({ id: '', userId: '42' });
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.NOTIFY_ALBUM_INVITE,
|
||||
data: { id: '', recipientId: '42' },
|
||||
});
|
||||
|
@ -192,67 +178,67 @@ describe(NotificationService.name, () => {
|
|||
it('should send a on_session_delete client event', () => {
|
||||
vi.useFakeTimers();
|
||||
sut.onSessionDelete({ sessionId: 'id' });
|
||||
expect(eventMock.clientSend).not.toHaveBeenCalled();
|
||||
expect(mocks.event.clientSend).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_session_delete', 'id', 'id');
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_session_delete', 'id', 'id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAssetTrash', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onAssetTrash({ assetId: 'asset-id', userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_trash', 'user-id', ['asset-id']);
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_trash', 'user-id', ['asset-id']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAssetDelete', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onAssetDelete({ assetId: 'asset-id', userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_delete', 'user-id', 'asset-id');
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_delete', 'user-id', 'asset-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAssetsTrash', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onAssetsTrash({ assetIds: ['asset-id'], userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_trash', 'user-id', ['asset-id']);
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_trash', 'user-id', ['asset-id']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAssetsRestore', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onAssetsRestore({ assetIds: ['asset-id'], userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_restore', 'user-id', ['asset-id']);
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_restore', 'user-id', ['asset-id']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onStackCreate', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onStackCreate({ stackId: 'stack-id', userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onStackUpdate', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onStackUpdate({ stackId: 'stack-id', userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onStackDelete', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onStackDelete({ stackId: 'stack-id', userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onStacksDelete', () => {
|
||||
it('should send connected clients an event', () => {
|
||||
sut.onStacksDelete({ stackIds: ['stack-id'], userId: 'user-id' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -262,8 +248,8 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should throw error if smtp validation fails', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
notificationMock.verifySmtp.mockRejectedValue('');
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.notification.verifySmtp.mockRejectedValue('');
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow(
|
||||
'Failed to verify SMTP configuration',
|
||||
|
@ -271,16 +257,16 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should send email to default domain', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
notificationMock.verifySmtp.mockResolvedValue(true);
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.notification.verifySmtp.mockResolvedValue(true);
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(notificationMock.renderEmail).toHaveBeenCalledWith({
|
||||
expect(mocks.notification.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'http://localhost:2283', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(notificationMock.sendEmail).toHaveBeenCalledWith(
|
||||
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
|
@ -289,17 +275,17 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should send email to external domain', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
notificationMock.verifySmtp.mockResolvedValue(true);
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
systemMock.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.notification.verifySmtp.mockResolvedValue(true);
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(notificationMock.renderEmail).toHaveBeenCalledWith({
|
||||
expect(mocks.notification.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(notificationMock.sendEmail).toHaveBeenCalledWith(
|
||||
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
|
@ -308,18 +294,18 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should send email with replyTo', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
notificationMock.verifySmtp.mockResolvedValue(true);
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.notification.verifySmtp.mockResolvedValue(true);
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await expect(
|
||||
sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),
|
||||
).resolves.not.toThrow();
|
||||
expect(notificationMock.renderEmail).toHaveBeenCalledWith({
|
||||
expect(mocks.notification.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'http://localhost:2283', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(notificationMock.sendEmail).toHaveBeenCalledWith(
|
||||
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
|
@ -335,12 +321,12 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should be successful', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
systemMock.get.mockResolvedValue({ server: {} });
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SEND_EMAIL,
|
||||
data: expect.objectContaining({ subject: 'Welcome to Immich' }),
|
||||
});
|
||||
|
@ -350,19 +336,19 @@ describe(NotificationService.name, () => {
|
|||
describe('handleAlbumInvite', () => {
|
||||
it('should skip if album could not be found', async () => {
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(userMock.get).not.toHaveBeenCalled();
|
||||
expect(mocks.user.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if recipient could not be found', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.empty);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(assetMock.getById).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if the recipient has email notifications disabled', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||
userMock.get.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue(albumStub.empty);
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
{
|
||||
|
@ -378,8 +364,8 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should skip if the recipient has email notifications for album invite disabled', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||
userMock.get.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue(albumStub.empty);
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
{
|
||||
|
@ -395,8 +381,8 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should send invite email', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.empty);
|
||||
userMock.get.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue(albumStub.empty);
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
{
|
||||
|
@ -407,19 +393,19 @@ describe(NotificationService.name, () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
systemMock.get.mockResolvedValue({ server: {} });
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SEND_EMAIL,
|
||||
data: expect.objectContaining({ subject: expect.stringContaining('You have been added to a shared album') }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should send invite email without album thumbnail if thumbnail asset does not exist', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
userMock.get.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
{
|
||||
|
@ -430,14 +416,14 @@ describe(NotificationService.name, () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
systemMock.get.mockResolvedValue({ server: {} });
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
|
||||
files: true,
|
||||
});
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SEND_EMAIL,
|
||||
data: expect.objectContaining({
|
||||
subject: expect.stringContaining('You have been added to a shared album'),
|
||||
|
@ -447,8 +433,8 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should send invite email with album thumbnail as jpeg', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
userMock.get.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
{
|
||||
|
@ -459,18 +445,18 @@ describe(NotificationService.name, () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
systemMock.get.mockResolvedValue({ server: {} });
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
assetMock.getById.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
files: [{ assetId: 'asset-id', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' } as AssetFileEntity],
|
||||
});
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
|
||||
files: true,
|
||||
});
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SEND_EMAIL,
|
||||
data: expect.objectContaining({
|
||||
subject: expect.stringContaining('You have been added to a shared album'),
|
||||
|
@ -480,8 +466,8 @@ describe(NotificationService.name, () => {
|
|||
});
|
||||
|
||||
it('should send invite email with album thumbnail and arbitrary extension', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
userMock.get.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
{
|
||||
|
@ -492,15 +478,15 @@ describe(NotificationService.name, () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
systemMock.get.mockResolvedValue({ server: {} });
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
|
||||
files: true,
|
||||
});
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SEND_EMAIL,
|
||||
data: expect.objectContaining({
|
||||
subject: expect.stringContaining('You have been added to a shared album'),
|
||||
|
@ -513,35 +499,35 @@ describe(NotificationService.name, () => {
|
|||
describe('handleAlbumUpdate', () => {
|
||||
it('should skip if album could not be found', async () => {
|
||||
await expect(sut.handleAlbumUpdate({ id: '', recipientIds: ['1'] })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(userMock.get).not.toHaveBeenCalled();
|
||||
expect(mocks.user.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if owner could not be found', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
|
||||
await expect(sut.handleAlbumUpdate({ id: '', recipientIds: ['1'] })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(systemMock.get).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip recipient that could not be looked up', async () => {
|
||||
albumMock.getById.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue({
|
||||
...albumStub.emptyWithValidThumbnail,
|
||||
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUserEntity],
|
||||
});
|
||||
userMock.get.mockResolvedValueOnce(userStub.user1);
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.user.get.mockResolvedValueOnce(userStub.user1);
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
|
||||
expect(notificationMock.renderEmail).not.toHaveBeenCalled();
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
|
||||
expect(mocks.notification.renderEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip recipient with disabled email notifications', async () => {
|
||||
albumMock.getById.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue({
|
||||
...albumStub.emptyWithValidThumbnail,
|
||||
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUserEntity],
|
||||
});
|
||||
userMock.get.mockResolvedValue({
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
{
|
||||
|
@ -552,19 +538,19 @@ describe(NotificationService.name, () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
|
||||
expect(notificationMock.renderEmail).not.toHaveBeenCalled();
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
|
||||
expect(mocks.notification.renderEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip recipient with disabled email notifications for the album update event', async () => {
|
||||
albumMock.getById.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue({
|
||||
...albumStub.emptyWithValidThumbnail,
|
||||
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUserEntity],
|
||||
});
|
||||
userMock.get.mockResolvedValue({
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
{
|
||||
|
@ -575,31 +561,31 @@ describe(NotificationService.name, () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
|
||||
expect(notificationMock.renderEmail).not.toHaveBeenCalled();
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
|
||||
expect(mocks.notification.renderEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send email', async () => {
|
||||
albumMock.getById.mockResolvedValue({
|
||||
mocks.album.getById.mockResolvedValue({
|
||||
...albumStub.emptyWithValidThumbnail,
|
||||
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUserEntity],
|
||||
});
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] });
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
|
||||
expect(notificationMock.renderEmail).toHaveBeenCalled();
|
||||
expect(jobMock.queue).toHaveBeenCalled();
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false });
|
||||
expect(mocks.notification.renderEmail).toHaveBeenCalled();
|
||||
expect(mocks.job.queue).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add new recipients for new images if job is already queued', async () => {
|
||||
jobMock.removeJob.mockResolvedValue({ id: '1', recipientIds: ['2', '3', '4'] } as INotifyAlbumUpdateJob);
|
||||
mocks.job.removeJob.mockResolvedValue({ id: '1', recipientIds: ['2', '3', '4'] } as INotifyAlbumUpdateJob);
|
||||
await sut.onAlbumUpdate({ id: '1', recipientIds: ['1', '2', '3'] } as INotifyAlbumUpdateJob);
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
data: {
|
||||
id: '1',
|
||||
|
@ -612,26 +598,32 @@ describe(NotificationService.name, () => {
|
|||
|
||||
describe('handleSendEmail', () => {
|
||||
it('should skip if smtp notifications are disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: false } } });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ notifications: { smtp: { enabled: false } } });
|
||||
await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SKIPPED);
|
||||
});
|
||||
|
||||
it('should send mail successfully', async () => {
|
||||
systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app' } } });
|
||||
notificationMock.sendEmail.mockResolvedValue({ messageId: '', response: '' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
notifications: { smtp: { enabled: true, from: 'test@immich.app' } },
|
||||
});
|
||||
mocks.notification.sendEmail.mockResolvedValue({ messageId: '', response: '' });
|
||||
|
||||
await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(notificationMock.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'test@immich.app' }));
|
||||
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ replyTo: 'test@immich.app' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send mail with replyTo successfully', async () => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
notifications: { smtp: { enabled: true, from: 'test@immich.app', replyTo: 'demo@immich.app' } },
|
||||
});
|
||||
notificationMock.sendEmail.mockResolvedValue({ messageId: '', response: '' });
|
||||
mocks.notification.sendEmail.mockResolvedValue({ messageId: '', response: '' });
|
||||
|
||||
await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(notificationMock.sendEmail).toHaveBeenCalledWith(expect.objectContaining({ replyTo: 'demo@immich.app' }));
|
||||
expect(mocks.notification.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ replyTo: 'demo@immich.app' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface';
|
||||
import { PartnerDirection } from 'src/interfaces/partner.interface';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(PartnerService.name, () => {
|
||||
let sut: PartnerService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let partnerMock: Mocked<IPartnerRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, partnerMock } = newTestService(PartnerService));
|
||||
({ sut, mocks } = newTestService(PartnerService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -23,55 +19,55 @@ describe(PartnerService.name, () => {
|
|||
|
||||
describe('search', () => {
|
||||
it("should return a list of partners with whom I've shared my library", async () => {
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
|
||||
mocks.partner.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
|
||||
await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedBy })).resolves.toBeDefined();
|
||||
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
expect(mocks.partner.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
});
|
||||
|
||||
it('should return a list of partners who have shared their libraries with me', async () => {
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
|
||||
mocks.partner.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
|
||||
await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
|
||||
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
expect(mocks.partner.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new partner', async () => {
|
||||
partnerMock.get.mockResolvedValue(void 0);
|
||||
partnerMock.create.mockResolvedValue(partnerStub.adminToUser1);
|
||||
mocks.partner.get.mockResolvedValue(void 0);
|
||||
mocks.partner.create.mockResolvedValue(partnerStub.adminToUser1);
|
||||
|
||||
await expect(sut.create(authStub.admin, authStub.user1.user.id)).resolves.toBeDefined();
|
||||
|
||||
expect(partnerMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.partner.create).toHaveBeenCalledWith({
|
||||
sharedById: authStub.admin.user.id,
|
||||
sharedWithId: authStub.user1.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when the partner already exists', async () => {
|
||||
partnerMock.get.mockResolvedValue(partnerStub.adminToUser1);
|
||||
mocks.partner.get.mockResolvedValue(partnerStub.adminToUser1);
|
||||
|
||||
await expect(sut.create(authStub.admin, authStub.user1.user.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(partnerMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.partner.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove a partner', async () => {
|
||||
partnerMock.get.mockResolvedValue(partnerStub.adminToUser1);
|
||||
mocks.partner.get.mockResolvedValue(partnerStub.adminToUser1);
|
||||
|
||||
await sut.remove(authStub.admin, authStub.user1.user.id);
|
||||
|
||||
expect(partnerMock.remove).toHaveBeenCalledWith(partnerStub.adminToUser1);
|
||||
expect(mocks.partner.remove).toHaveBeenCalledWith(partnerStub.adminToUser1);
|
||||
});
|
||||
|
||||
it('should throw an error when the partner does not exist', async () => {
|
||||
partnerMock.get.mockResolvedValue(void 0);
|
||||
mocks.partner.get.mockResolvedValue(void 0);
|
||||
|
||||
await expect(sut.remove(authStub.admin, authStub.user1.user.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(partnerMock.remove).not.toHaveBeenCalled();
|
||||
expect(mocks.partner.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -83,11 +79,11 @@ describe(PartnerService.name, () => {
|
|||
});
|
||||
|
||||
it('should update partner', async () => {
|
||||
accessMock.partner.checkUpdateAccess.mockResolvedValue(new Set(['shared-by-id']));
|
||||
partnerMock.update.mockResolvedValue(partnerStub.adminToUser1);
|
||||
mocks.access.partner.checkUpdateAccess.mockResolvedValue(new Set(['shared-by-id']));
|
||||
mocks.partner.update.mockResolvedValue(partnerStub.adminToUser1);
|
||||
|
||||
await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: true })).resolves.toBeDefined();
|
||||
expect(partnerMock.update).toHaveBeenCalledWith(
|
||||
expect(mocks.partner.update).toHaveBeenCalledWith(
|
||||
{ sharedById: 'shared-by-id', sharedWithId: authStub.admin.user.id },
|
||||
{ inTimeline: true },
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,26 +1,20 @@
|
|||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SearchSuggestionType } from 'src/dtos/search.dto';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked, beforeEach, vitest } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { beforeEach, vitest } from 'vitest';
|
||||
|
||||
vitest.useFakeTimers();
|
||||
|
||||
describe(SearchService.name, () => {
|
||||
let sut: SearchService;
|
||||
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
let searchMock: Mocked<ISearchRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, assetMock, personMock, searchMock } = newTestService(SearchService));
|
||||
({ sut, mocks } = newTestService(SearchService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -31,25 +25,25 @@ describe(SearchService.name, () => {
|
|||
it('should pass options to search', async () => {
|
||||
const { name } = personStub.withName;
|
||||
|
||||
personMock.getByName.mockResolvedValue([]);
|
||||
mocks.person.getByName.mockResolvedValue([]);
|
||||
|
||||
await sut.searchPerson(authStub.user1, { name, withHidden: false });
|
||||
|
||||
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false });
|
||||
expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false });
|
||||
|
||||
await sut.searchPerson(authStub.user1, { name, withHidden: true });
|
||||
|
||||
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true });
|
||||
expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExploreData', () => {
|
||||
it('should get assets by city and tag', async () => {
|
||||
assetMock.getAssetIdByCity.mockResolvedValue({
|
||||
mocks.asset.getAssetIdByCity.mockResolvedValue({
|
||||
fieldName: 'exifInfo.city',
|
||||
items: [{ value: 'test-city', data: assetStub.withLocation.id }],
|
||||
});
|
||||
assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.withLocation]);
|
||||
mocks.asset.getByIdsWithAllRelations.mockResolvedValue([assetStub.withLocation]);
|
||||
const expectedResponse = [
|
||||
{ fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: mapAsset(assetStub.withLocation) }] },
|
||||
];
|
||||
|
@ -62,83 +56,83 @@ describe(SearchService.name, () => {
|
|||
|
||||
describe('getSearchSuggestions', () => {
|
||||
it('should return search suggestions for country', async () => {
|
||||
searchMock.getCountries.mockResolvedValue(['USA']);
|
||||
mocks.search.getCountries.mockResolvedValue(['USA']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }),
|
||||
).resolves.toEqual(['USA']);
|
||||
expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
|
||||
expect(mocks.search.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
|
||||
});
|
||||
|
||||
it('should return search suggestions for country (including null)', async () => {
|
||||
searchMock.getCountries.mockResolvedValue(['USA']);
|
||||
mocks.search.getCountries.mockResolvedValue(['USA']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }),
|
||||
).resolves.toEqual(['USA', null]);
|
||||
expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
|
||||
expect(mocks.search.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
|
||||
});
|
||||
|
||||
it('should return search suggestions for state', async () => {
|
||||
searchMock.getStates.mockResolvedValue(['California']);
|
||||
mocks.search.getStates.mockResolvedValue(['California']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.STATE }),
|
||||
).resolves.toEqual(['California']);
|
||||
expect(searchMock.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
expect(mocks.search.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
});
|
||||
|
||||
it('should return search suggestions for state (including null)', async () => {
|
||||
searchMock.getStates.mockResolvedValue(['California']);
|
||||
mocks.search.getStates.mockResolvedValue(['California']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.STATE }),
|
||||
).resolves.toEqual(['California', null]);
|
||||
expect(searchMock.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
expect(mocks.search.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
});
|
||||
|
||||
it('should return search suggestions for city', async () => {
|
||||
searchMock.getCities.mockResolvedValue(['Denver']);
|
||||
mocks.search.getCities.mockResolvedValue(['Denver']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CITY }),
|
||||
).resolves.toEqual(['Denver']);
|
||||
expect(searchMock.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
expect(mocks.search.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
});
|
||||
|
||||
it('should return search suggestions for city (including null)', async () => {
|
||||
searchMock.getCities.mockResolvedValue(['Denver']);
|
||||
mocks.search.getCities.mockResolvedValue(['Denver']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CITY }),
|
||||
).resolves.toEqual(['Denver', null]);
|
||||
expect(searchMock.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
expect(mocks.search.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
});
|
||||
|
||||
it('should return search suggestions for camera make', async () => {
|
||||
searchMock.getCameraMakes.mockResolvedValue(['Nikon']);
|
||||
mocks.search.getCameraMakes.mockResolvedValue(['Nikon']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MAKE }),
|
||||
).resolves.toEqual(['Nikon']);
|
||||
expect(searchMock.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
expect(mocks.search.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
});
|
||||
|
||||
it('should return search suggestions for camera make (including null)', async () => {
|
||||
searchMock.getCameraMakes.mockResolvedValue(['Nikon']);
|
||||
mocks.search.getCameraMakes.mockResolvedValue(['Nikon']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MAKE }),
|
||||
).resolves.toEqual(['Nikon', null]);
|
||||
expect(searchMock.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
expect(mocks.search.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
});
|
||||
|
||||
it('should return search suggestions for camera model', async () => {
|
||||
searchMock.getCameraModels.mockResolvedValue(['Fujifilm X100VI']);
|
||||
mocks.search.getCameraModels.mockResolvedValue(['Fujifilm X100VI']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MODEL }),
|
||||
).resolves.toEqual(['Fujifilm X100VI']);
|
||||
expect(searchMock.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
expect(mocks.search.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
});
|
||||
|
||||
it('should return search suggestions for camera model (including null)', async () => {
|
||||
searchMock.getCameraModels.mockResolvedValue(['Fujifilm X100VI']);
|
||||
mocks.search.getCameraModels.mockResolvedValue(['Fujifilm X100VI']);
|
||||
await expect(
|
||||
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MODEL }),
|
||||
).resolves.toEqual(['Fujifilm X100VI', null]);
|
||||
expect(searchMock.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
expect(mocks.search.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { ServerService } from 'src/services/server.service';
|
||||
import { ISystemMetadataRepository } from 'src/types';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(ServerService.name, () => {
|
||||
let sut: ServerService;
|
||||
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, storageMock, systemMock, userMock } = newTestService(ServerService));
|
||||
({ sut, mocks } = newTestService(ServerService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -23,7 +16,7 @@ describe(ServerService.name, () => {
|
|||
|
||||
describe('getStorage', () => {
|
||||
it('should return the disk space as B', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({ free: 200, available: 300, total: 500 });
|
||||
mocks.storage.checkDiskUsage.mockResolvedValue({ free: 200, available: 300, total: 500 });
|
||||
|
||||
await expect(sut.getStorage()).resolves.toEqual({
|
||||
diskAvailable: '300 B',
|
||||
|
@ -35,11 +28,11 @@ describe(ServerService.name, () => {
|
|||
diskUseRaw: 300,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
});
|
||||
|
||||
it('should return the disk space as KiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({ free: 200_000, available: 300_000, total: 500_000 });
|
||||
mocks.storage.checkDiskUsage.mockResolvedValue({ free: 200_000, available: 300_000, total: 500_000 });
|
||||
|
||||
await expect(sut.getStorage()).resolves.toEqual({
|
||||
diskAvailable: '293.0 KiB',
|
||||
|
@ -51,11 +44,11 @@ describe(ServerService.name, () => {
|
|||
diskUseRaw: 300_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
});
|
||||
|
||||
it('should return the disk space as MiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({ free: 200_000_000, available: 300_000_000, total: 500_000_000 });
|
||||
mocks.storage.checkDiskUsage.mockResolvedValue({ free: 200_000_000, available: 300_000_000, total: 500_000_000 });
|
||||
|
||||
await expect(sut.getStorage()).resolves.toEqual({
|
||||
diskAvailable: '286.1 MiB',
|
||||
|
@ -67,11 +60,11 @@ describe(ServerService.name, () => {
|
|||
diskUseRaw: 300_000_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
});
|
||||
|
||||
it('should return the disk space as GiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({
|
||||
mocks.storage.checkDiskUsage.mockResolvedValue({
|
||||
free: 200_000_000_000,
|
||||
available: 300_000_000_000,
|
||||
total: 500_000_000_000,
|
||||
|
@ -87,11 +80,11 @@ describe(ServerService.name, () => {
|
|||
diskUseRaw: 300_000_000_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
});
|
||||
|
||||
it('should return the disk space as TiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({
|
||||
mocks.storage.checkDiskUsage.mockResolvedValue({
|
||||
free: 200_000_000_000_000,
|
||||
available: 300_000_000_000_000,
|
||||
total: 500_000_000_000_000,
|
||||
|
@ -107,11 +100,11 @@ describe(ServerService.name, () => {
|
|||
diskUseRaw: 300_000_000_000_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
});
|
||||
|
||||
it('should return the disk space as PiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({
|
||||
mocks.storage.checkDiskUsage.mockResolvedValue({
|
||||
free: 200_000_000_000_000_000,
|
||||
available: 300_000_000_000_000_000,
|
||||
total: 500_000_000_000_000_000,
|
||||
|
@ -127,7 +120,7 @@ describe(ServerService.name, () => {
|
|||
diskUseRaw: 300_000_000_000_000_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
expect(mocks.storage.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -155,7 +148,7 @@ describe(ServerService.name, () => {
|
|||
trash: true,
|
||||
email: false,
|
||||
});
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -173,13 +166,13 @@ describe(ServerService.name, () => {
|
|||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
});
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should total up usage by user', async () => {
|
||||
userMock.getUserStats.mockResolvedValue([
|
||||
mocks.user.getUserStats.mockResolvedValue([
|
||||
{
|
||||
userId: 'user1',
|
||||
userName: '1 User',
|
||||
|
@ -252,36 +245,36 @@ describe(ServerService.name, () => {
|
|||
],
|
||||
});
|
||||
|
||||
expect(userMock.getUserStats).toHaveBeenCalled();
|
||||
expect(mocks.user.getUserStats).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLicense', () => {
|
||||
it('should save license if valid', async () => {
|
||||
systemMock.set.mockResolvedValue();
|
||||
mocks.systemMetadata.set.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'IMSV-license-key', activationKey: 'activation-key' };
|
||||
await sut.setLicense(license);
|
||||
|
||||
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.LICENSE, expect.any(Object));
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.LICENSE, expect.any(Object));
|
||||
});
|
||||
|
||||
it('should not save license if invalid', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
mocks.user.upsertMetadata.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'license-key', activationKey: 'activation-key' };
|
||||
const call = sut.setLicense(license);
|
||||
await expect(call).rejects.toThrowError('Invalid license key');
|
||||
expect(userMock.upsertMetadata).not.toHaveBeenCalled();
|
||||
expect(mocks.user.upsertMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteLicense', () => {
|
||||
it('should delete license', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
mocks.user.upsertMetadata.mockResolvedValue();
|
||||
|
||||
await sut.deleteLicense();
|
||||
expect(userMock.upsertMetadata).not.toHaveBeenCalled();
|
||||
expect(mocks.user.upsertMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { SessionService } from 'src/services/session.service';
|
||||
import { ISessionRepository } from 'src/types';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sessionStub } from 'test/fixtures/session.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe('SessionService', () => {
|
||||
let sut: SessionService;
|
||||
|
||||
let accessMock: Mocked<IAccessRepositoryMock>;
|
||||
let sessionMock: Mocked<ISessionRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, sessionMock } = newTestService(SessionService));
|
||||
({ sut, mocks } = newTestService(SessionService));
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
@ -23,13 +18,13 @@ describe('SessionService', () => {
|
|||
|
||||
describe('handleCleanup', () => {
|
||||
it('should return skipped if nothing is to be deleted', async () => {
|
||||
sessionMock.search.mockResolvedValue([]);
|
||||
mocks.session.search.mockResolvedValue([]);
|
||||
await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SKIPPED);
|
||||
expect(sessionMock.search).toHaveBeenCalled();
|
||||
expect(mocks.session.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete sessions', async () => {
|
||||
sessionMock.search.mockResolvedValue([
|
||||
mocks.session.search.mockResolvedValue([
|
||||
{
|
||||
createdAt: new Date('1970-01-01T00:00:00.00Z'),
|
||||
updatedAt: new Date('1970-01-02T00:00:00.00Z'),
|
||||
|
@ -42,13 +37,13 @@ describe('SessionService', () => {
|
|||
]);
|
||||
|
||||
await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
expect(sessionMock.delete).toHaveBeenCalledWith('123');
|
||||
expect(mocks.session.delete).toHaveBeenCalledWith('123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should get the devices', async () => {
|
||||
sessionMock.getByUserId.mockResolvedValue([sessionStub.valid as any, sessionStub.inactive]);
|
||||
mocks.session.getByUserId.mockResolvedValue([sessionStub.valid as any, sessionStub.inactive]);
|
||||
await expect(sut.getAll(authStub.user1)).resolves.toEqual([
|
||||
{
|
||||
createdAt: '2021-01-01T00:00:00.000Z',
|
||||
|
@ -68,30 +63,33 @@ describe('SessionService', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
expect(sessionMock.getByUserId).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
expect(mocks.session.getByUserId).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logoutDevices', () => {
|
||||
it('should logout all devices', async () => {
|
||||
sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid] as any[]);
|
||||
mocks.session.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid] as any[]);
|
||||
|
||||
await sut.deleteAll(authStub.user1);
|
||||
|
||||
expect(sessionMock.getByUserId).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
expect(sessionMock.delete).toHaveBeenCalledWith('not_active');
|
||||
expect(sessionMock.delete).not.toHaveBeenCalledWith('token-id');
|
||||
expect(mocks.session.getByUserId).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
expect(mocks.session.delete).toHaveBeenCalledWith('not_active');
|
||||
expect(mocks.session.delete).not.toHaveBeenCalledWith('token-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logoutDevice', () => {
|
||||
it('should logout the device', async () => {
|
||||
accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1']));
|
||||
mocks.access.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1']));
|
||||
|
||||
await sut.delete(authStub.user1, 'token-1');
|
||||
|
||||
expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1']));
|
||||
expect(sessionMock.delete).toHaveBeenCalledWith('token-1');
|
||||
expect(mocks.access.authDevice.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.user1.user.id,
|
||||
new Set(['token-1']),
|
||||
);
|
||||
expect(mocks.session.delete).toHaveBeenCalledWith('token-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,24 +2,19 @@ import { BadRequestException, ForbiddenException, UnauthorizedException } from '
|
|||
import _ from 'lodash';
|
||||
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(SharedLinkService.name, () => {
|
||||
let sut: SharedLinkService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let sharedLinkMock: Mocked<ISharedLinkRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, sharedLinkMock } = newTestService(SharedLinkService));
|
||||
({ sut, mocks } = newTestService(SharedLinkService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -28,46 +23,46 @@ describe(SharedLinkService.name, () => {
|
|||
|
||||
describe('getAll', () => {
|
||||
it('should return all shared links for a user', async () => {
|
||||
sharedLinkMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
|
||||
mocks.sharedLink.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
|
||||
await expect(sut.getAll(authStub.user1, {})).resolves.toEqual([
|
||||
sharedLinkResponseStub.expired,
|
||||
sharedLinkResponseStub.valid,
|
||||
]);
|
||||
expect(sharedLinkMock.getAll).toHaveBeenCalledWith({ userId: authStub.user1.user.id });
|
||||
expect(mocks.sharedLink.getAll).toHaveBeenCalledWith({ userId: authStub.user1.user.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMine', () => {
|
||||
it('should only work for a public user', async () => {
|
||||
await expect(sut.getMine(authStub.admin, {})).rejects.toBeInstanceOf(ForbiddenException);
|
||||
expect(sharedLinkMock.get).not.toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return the shared link for the public user', async () => {
|
||||
const authDto = authStub.adminSharedLink;
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
});
|
||||
|
||||
it('should not return metadata', async () => {
|
||||
const authDto = authStub.adminSharedLinkNoExif;
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
||||
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata);
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
});
|
||||
|
||||
it('should throw an error for an invalid password protected shared link', async () => {
|
||||
const authDto = authStub.adminSharedLink;
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.passwordRequired);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.passwordRequired);
|
||||
await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
});
|
||||
|
||||
it('should allow a correct password on a password protected shared link', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' });
|
||||
mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' });
|
||||
await expect(sut.getMine(authStub.adminSharedLink, { password: '123' })).resolves.toBeDefined();
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.user.id,
|
||||
authStub.adminSharedLink.sharedLink?.id,
|
||||
);
|
||||
|
@ -77,14 +72,14 @@ describe(SharedLinkService.name, () => {
|
|||
describe('get', () => {
|
||||
it('should throw an error for an invalid shared link', async () => {
|
||||
await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
|
||||
expect(sharedLinkMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
|
||||
expect(mocks.sharedLink.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should get a shared link by id', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -114,16 +109,16 @@ describe(SharedLinkService.name, () => {
|
|||
});
|
||||
|
||||
it('should create an album shared link', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id]));
|
||||
sharedLinkMock.create.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id]));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.valid);
|
||||
|
||||
await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id });
|
||||
|
||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([albumStub.oneAsset.id]),
|
||||
);
|
||||
expect(sharedLinkMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||
type: SharedLinkType.ALBUM,
|
||||
userId: authStub.admin.user.id,
|
||||
albumId: albumStub.oneAsset.id,
|
||||
|
@ -137,8 +132,8 @@ describe(SharedLinkService.name, () => {
|
|||
});
|
||||
|
||||
it('should create an individual shared link', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
type: SharedLinkType.INDIVIDUAL,
|
||||
|
@ -148,11 +143,11 @@ describe(SharedLinkService.name, () => {
|
|||
allowUpload: true,
|
||||
});
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
expect(sharedLinkMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||
type: SharedLinkType.INDIVIDUAL,
|
||||
userId: authStub.admin.user.id,
|
||||
albumId: null,
|
||||
|
@ -167,8 +162,8 @@ describe(SharedLinkService.name, () => {
|
|||
});
|
||||
|
||||
it('should create a shared link with allowDownload set to false when showMetadata is false', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
type: SharedLinkType.INDIVIDUAL,
|
||||
|
@ -178,11 +173,11 @@ describe(SharedLinkService.name, () => {
|
|||
allowUpload: true,
|
||||
});
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
expect(sharedLinkMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||
type: SharedLinkType.INDIVIDUAL,
|
||||
userId: authStub.admin.user.id,
|
||||
albumId: null,
|
||||
|
@ -200,16 +195,16 @@ describe(SharedLinkService.name, () => {
|
|||
describe('update', () => {
|
||||
it('should throw an error for an invalid shared link', async () => {
|
||||
await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
|
||||
expect(sharedLinkMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
|
||||
expect(mocks.sharedLink.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update a shared link', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
sharedLinkMock.update.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.valid);
|
||||
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
expect(sharedLinkMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
||||
id: sharedLinkStub.valid.id,
|
||||
userId: authStub.user1.user.id,
|
||||
allowDownload: false,
|
||||
|
@ -220,30 +215,30 @@ describe(SharedLinkService.name, () => {
|
|||
describe('remove', () => {
|
||||
it('should throw an error for an invalid shared link', async () => {
|
||||
await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
|
||||
expect(sharedLinkMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
|
||||
expect(mocks.sharedLink.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove a key', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await sut.remove(authStub.user1, sharedLinkStub.valid.id);
|
||||
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
expect(sharedLinkMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addAssets', () => {
|
||||
it('should not work on album shared links', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should add assets to a shared link', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
|
||||
sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3']));
|
||||
mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3']));
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2', 'asset-3'] }),
|
||||
|
@ -253,9 +248,9 @@ describe(SharedLinkService.name, () => {
|
|||
{ assetId: 'asset-3', success: true },
|
||||
]);
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
|
||||
expect(sharedLinkMock.update).toHaveBeenCalled();
|
||||
expect(sharedLinkMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
||||
...sharedLinkStub.individual,
|
||||
assetIds: ['asset-3'],
|
||||
});
|
||||
|
@ -264,15 +259,15 @@ describe(SharedLinkService.name, () => {
|
|||
|
||||
describe('removeAssets', () => {
|
||||
it('should not work on album shared links', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove assets from a shared link', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
|
||||
sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
|
||||
await expect(
|
||||
sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }),
|
||||
|
@ -281,39 +276,39 @@ describe(SharedLinkService.name, () => {
|
|||
{ assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND },
|
||||
]);
|
||||
|
||||
expect(sharedLinkMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] });
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMetadataTags', () => {
|
||||
it('should return null when auth is not a shared link', async () => {
|
||||
await expect(sut.getMetadataTags(authStub.admin)).resolves.toBe(null);
|
||||
expect(sharedLinkMock.get).not.toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null when shared link has a password', async () => {
|
||||
await expect(sut.getMetadataTags(authStub.passwordSharedLink)).resolves.toBe(null);
|
||||
expect(sharedLinkMock.get).not.toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return metadata tags', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.individual);
|
||||
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
|
||||
description: '1 shared photos & videos',
|
||||
imageUrl: `http://localhost:2283/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
|
||||
title: 'Public Share',
|
||||
});
|
||||
expect(sharedLinkMock.get).toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return metadata tags with a default image path if the asset id is not set', async () => {
|
||||
sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
|
||||
mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
|
||||
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
|
||||
description: '0 shared photos & videos',
|
||||
imageUrl: `http://localhost:2283/feature-panel.png`,
|
||||
title: 'Public Share',
|
||||
});
|
||||
expect(sharedLinkMock.get).toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,35 +1,22 @@
|
|||
import { SystemConfig } from 'src/config';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { SmartInfoService } from 'src/services/smart-info.service';
|
||||
import { IConfigRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { getCLIPModelInfo } from 'src/utils/misc';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(SmartInfoService.name, () => {
|
||||
let sut: SmartInfoService;
|
||||
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let machineLearningMock: Mocked<IMachineLearningRepository>;
|
||||
let searchMock: Mocked<ISearchRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, assetMock, databaseMock, jobMock, machineLearningMock, searchMock, systemMock, configMock } =
|
||||
newTestService(SmartInfoService));
|
||||
({ sut, mocks } = newTestService(SmartInfoService));
|
||||
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -69,79 +56,79 @@ describe(SmartInfoService.name, () => {
|
|||
it('should return if machine learning is disabled', async () => {
|
||||
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningDisabled as SystemConfig });
|
||||
|
||||
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(jobMock.getQueueStatus).not.toHaveBeenCalled();
|
||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||
expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||
expect(mocks.search.getDimensionSize).not.toHaveBeenCalled();
|
||||
expect(mocks.search.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(mocks.job.getQueueStatus).not.toHaveBeenCalled();
|
||||
expect(mocks.job.pause).not.toHaveBeenCalled();
|
||||
expect(mocks.job.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||
expect(mocks.job.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return if model and DB dimension size are equal', async () => {
|
||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||
mocks.search.getDimensionSize.mockResolvedValue(512);
|
||||
|
||||
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
|
||||
|
||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(jobMock.getQueueStatus).not.toHaveBeenCalled();
|
||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||
expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||
expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(mocks.job.getQueueStatus).not.toHaveBeenCalled();
|
||||
expect(mocks.job.pause).not.toHaveBeenCalled();
|
||||
expect(mocks.job.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||
expect(mocks.job.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update DB dimension size if model and DB have different values', async () => {
|
||||
searchMock.getDimensionSize.mockResolvedValue(768);
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.search.getDimensionSize.mockResolvedValue(768);
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
|
||||
|
||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512);
|
||||
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.setDimensionSize).toHaveBeenCalledWith(512);
|
||||
expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.pause).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.resume).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should skip pausing and resuming queue if already paused', async () => {
|
||||
searchMock.getDimensionSize.mockResolvedValue(768);
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
||||
mocks.search.getDimensionSize.mockResolvedValue(768);
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
||||
|
||||
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
|
||||
|
||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512);
|
||||
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||
expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||
expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.setDimensionSize).toHaveBeenCalledWith(512);
|
||||
expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.pause).not.toHaveBeenCalled();
|
||||
expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onConfigUpdateEvent', () => {
|
||||
it('should return if machine learning is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
await sut.onConfigUpdate({
|
||||
newConfig: systemConfigStub.machineLearningDisabled as SystemConfig,
|
||||
oldConfig: systemConfigStub.machineLearningDisabled as SystemConfig,
|
||||
});
|
||||
|
||||
expect(systemMock.get).not.toHaveBeenCalled();
|
||||
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(jobMock.getQueueStatus).not.toHaveBeenCalled();
|
||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||
expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
|
||||
expect(mocks.search.getDimensionSize).not.toHaveBeenCalled();
|
||||
expect(mocks.search.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(mocks.job.getQueueStatus).not.toHaveBeenCalled();
|
||||
expect(mocks.job.pause).not.toHaveBeenCalled();
|
||||
expect(mocks.job.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||
expect(mocks.job.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return if model and DB dimension size are equal', async () => {
|
||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||
mocks.search.getDimensionSize.mockResolvedValue(512);
|
||||
|
||||
await sut.onConfigUpdate({
|
||||
newConfig: {
|
||||
|
@ -152,18 +139,18 @@ describe(SmartInfoService.name, () => {
|
|||
} as SystemConfig,
|
||||
});
|
||||
|
||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(jobMock.getQueueStatus).not.toHaveBeenCalled();
|
||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||
expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||
expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(mocks.job.getQueueStatus).not.toHaveBeenCalled();
|
||||
expect(mocks.job.pause).not.toHaveBeenCalled();
|
||||
expect(mocks.job.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||
expect(mocks.job.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update DB dimension size if model and DB have different values', async () => {
|
||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.search.getDimensionSize.mockResolvedValue(512);
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.onConfigUpdate({
|
||||
newConfig: {
|
||||
|
@ -174,17 +161,17 @@ describe(SmartInfoService.name, () => {
|
|||
} as SystemConfig,
|
||||
});
|
||||
|
||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(searchMock.setDimensionSize).toHaveBeenCalledWith(768);
|
||||
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.setDimensionSize).toHaveBeenCalledWith(768);
|
||||
expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.pause).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.resume).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should clear embeddings if old and new models are different', async () => {
|
||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.search.getDimensionSize.mockResolvedValue(512);
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.onConfigUpdate({
|
||||
newConfig: {
|
||||
|
@ -195,18 +182,18 @@ describe(SmartInfoService.name, () => {
|
|||
} as SystemConfig,
|
||||
});
|
||||
|
||||
expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled();
|
||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.deleteAllSearchEmbeddings).toHaveBeenCalled();
|
||||
expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.pause).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.resume).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should skip pausing and resuming queue if already paused', async () => {
|
||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
||||
mocks.search.getDimensionSize.mockResolvedValue(512);
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
||||
|
||||
await sut.onConfigUpdate({
|
||||
newConfig: {
|
||||
|
@ -217,115 +204,119 @@ describe(SmartInfoService.name, () => {
|
|||
} as SystemConfig,
|
||||
});
|
||||
|
||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||
expect(jobMock.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||
expect(mocks.search.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.search.setDimensionSize).not.toHaveBeenCalled();
|
||||
expect(mocks.job.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.pause).not.toHaveBeenCalled();
|
||||
expect(mocks.job.waitForQueueCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueEncodeClip', () => {
|
||||
it('should do nothing if machine learning is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
await sut.handleQueueEncodeClip({});
|
||||
|
||||
expect(assetMock.getAll).not.toHaveBeenCalled();
|
||||
expect(assetMock.getWithout).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getAll).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getWithout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue the assets without clip embeddings', async () => {
|
||||
assetMock.getWithout.mockResolvedValue({
|
||||
mocks.asset.getWithout.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
await sut.handleQueueEncodeClip({ force: false });
|
||||
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
|
||||
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH);
|
||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } },
|
||||
]);
|
||||
expect(mocks.asset.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH);
|
||||
expect(mocks.search.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue all the assets', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
await sut.handleQueueEncodeClip({ force: true });
|
||||
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } },
|
||||
]);
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.search.deleteAllSearchEmbeddings).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleEncodeClip', () => {
|
||||
it('should do nothing if machine learning is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED);
|
||||
|
||||
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
||||
expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getByIds).not.toHaveBeenCalled();
|
||||
expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip assets without a resize path', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.noResizePath]);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED);
|
||||
|
||||
expect(searchMock.upsert).not.toHaveBeenCalled();
|
||||
expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
|
||||
expect(mocks.search.upsert).not.toHaveBeenCalled();
|
||||
expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save the returned objects', async () => {
|
||||
machineLearningMock.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
|
||||
|
||||
expect(machineLearningMock.encodeImage).toHaveBeenCalledWith(
|
||||
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
||||
['http://immich-machine-learning:3003'],
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||
);
|
||||
expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
|
||||
expect(mocks.search.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
|
||||
});
|
||||
|
||||
it('should skip invisible assets', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
|
||||
|
||||
expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
|
||||
expect(searchMock.upsert).not.toHaveBeenCalled();
|
||||
expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled();
|
||||
expect(mocks.search.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if asset could not be found', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([]);
|
||||
mocks.asset.getByIds.mockResolvedValue([]);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.FAILED);
|
||||
|
||||
expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
|
||||
expect(searchMock.upsert).not.toHaveBeenCalled();
|
||||
expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled();
|
||||
expect(mocks.search.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should wait for database', async () => {
|
||||
machineLearningMock.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
databaseMock.isBusy.mockReturnValue(true);
|
||||
mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
mocks.database.isBusy.mockReturnValue(true);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
|
||||
|
||||
expect(databaseMock.wait).toHaveBeenCalledWith(512);
|
||||
expect(machineLearningMock.encodeImage).toHaveBeenCalledWith(
|
||||
expect(mocks.database.wait).toHaveBeenCalledWith(512);
|
||||
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
||||
['http://immich-machine-learning:3003'],
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||
);
|
||||
expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
|
||||
expect(mocks.search.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { StackService } from 'src/services/stack.service';
|
||||
import { assetStub, stackStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(StackService.name, () => {
|
||||
let sut: StackService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let stackMock: Mocked<IStackRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, eventMock, stackMock } = newTestService(StackService));
|
||||
({ sut, mocks } = newTestService(StackService));
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
@ -25,10 +18,10 @@ describe(StackService.name, () => {
|
|||
|
||||
describe('search', () => {
|
||||
it('should search stacks', async () => {
|
||||
stackMock.search.mockResolvedValue([stackStub('stack-id', [assetStub.image])]);
|
||||
mocks.stack.search.mockResolvedValue([stackStub('stack-id', [assetStub.image])]);
|
||||
|
||||
await sut.search(authStub.admin, { primaryAssetId: assetStub.image.id });
|
||||
expect(stackMock.search).toHaveBeenCalledWith({
|
||||
expect(mocks.stack.search).toHaveBeenCalledWith({
|
||||
ownerId: authStub.admin.user.id,
|
||||
primaryAssetId: assetStub.image.id,
|
||||
});
|
||||
|
@ -41,13 +34,13 @@ describe(StackService.name, () => {
|
|||
sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(stackMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.stack.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a stack', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id, assetStub.image1.id]));
|
||||
stackMock.create.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id, assetStub.image1.id]));
|
||||
mocks.stack.create.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
await expect(
|
||||
sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }),
|
||||
).resolves.toEqual({
|
||||
|
@ -59,11 +52,11 @@ describe(StackService.name, () => {
|
|||
],
|
||||
});
|
||||
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('stack.create', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('stack.create', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -71,22 +64,22 @@ describe(StackService.name, () => {
|
|||
it('should require stack.read permissions', async () => {
|
||||
await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.stack.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(stackMock.getById).not.toHaveBeenCalled();
|
||||
expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.stack.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if stack could not be found', async () => {
|
||||
accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
|
||||
await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(Error);
|
||||
|
||||
expect(accessMock.stack.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
});
|
||||
|
||||
it('should get stack', async () => {
|
||||
accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
stackMock.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
|
||||
await expect(sut.get(authStub.admin, 'stack-id')).resolves.toEqual({
|
||||
id: 'stack-id',
|
||||
|
@ -96,8 +89,8 @@ describe(StackService.name, () => {
|
|||
expect.objectContaining({ id: assetStub.image1.id }),
|
||||
],
|
||||
});
|
||||
expect(accessMock.stack.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -105,47 +98,47 @@ describe(StackService.name, () => {
|
|||
it('should require stack.update permissions', async () => {
|
||||
await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(stackMock.getById).not.toHaveBeenCalled();
|
||||
expect(stackMock.update).not.toHaveBeenCalled();
|
||||
expect(eventMock.emit).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.getById).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.update).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if stack could not be found', async () => {
|
||||
accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
|
||||
await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(Error);
|
||||
|
||||
expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(stackMock.update).not.toHaveBeenCalled();
|
||||
expect(eventMock.emit).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.update).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if the provided primary asset id is not in the stack', async () => {
|
||||
accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
stackMock.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
|
||||
await expect(sut.update(authStub.admin, 'stack-id', { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(stackMock.update).not.toHaveBeenCalled();
|
||||
expect(eventMock.emit).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.update).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update stack', async () => {
|
||||
accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
stackMock.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
stackMock.update.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
mocks.stack.update.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
|
||||
await sut.update(authStub.admin, 'stack-id', { primaryAssetId: assetStub.image1.id });
|
||||
|
||||
expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(stackMock.update).toHaveBeenCalledWith('stack-id', {
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.update).toHaveBeenCalledWith('stack-id', {
|
||||
id: 'stack-id',
|
||||
primaryAssetId: assetStub.image1.id,
|
||||
});
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('stack.update', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('stack.update', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
|
@ -156,17 +149,17 @@ describe(StackService.name, () => {
|
|||
it('should require stack.delete permissions', async () => {
|
||||
await expect(sut.delete(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(stackMock.delete).not.toHaveBeenCalled();
|
||||
expect(eventMock.emit).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete stack', async () => {
|
||||
accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
|
||||
await sut.delete(authStub.admin, 'stack-id');
|
||||
|
||||
expect(stackMock.delete).toHaveBeenCalledWith('stack-id');
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('stack.delete', {
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('stack.delete', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
|
@ -177,17 +170,17 @@ describe(StackService.name, () => {
|
|||
it('should require stack.delete permissions', async () => {
|
||||
await expect(sut.deleteAll(authStub.admin, { ids: ['stack-id'] })).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(stackMock.deleteAll).not.toHaveBeenCalled();
|
||||
expect(eventMock.emit).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.deleteAll).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete all stacks', async () => {
|
||||
accessMock.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
|
||||
await sut.deleteAll(authStub.admin, { ids: ['stack-id'] });
|
||||
|
||||
expect(stackMock.deleteAll).toHaveBeenCalledWith(['stack-id']);
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('stacks.delete', {
|
||||
expect(mocks.stack.deleteAll).toHaveBeenCalledWith(['stack-id']);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('stacks.delete', {
|
||||
stackIds: ['stack-id'],
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
|
|
|
@ -1,42 +1,26 @@
|
|||
import { Stats } from 'node:fs';
|
||||
import { SystemConfig, defaults } from 'src/config';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetPathType } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||
import { ISystemMetadataRepository } from 'src/types';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(StorageTemplateService.name, () => {
|
||||
let sut: StorageTemplateService;
|
||||
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let moveMock: Mocked<IMoveRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, albumMock, assetMock, cryptoMock, moveMock, storageMock, systemMock, userMock } =
|
||||
newTestService(StorageTemplateService));
|
||||
({ sut, mocks } = newTestService(StorageTemplateService));
|
||||
|
||||
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ storageTemplate: { enabled: true } });
|
||||
|
||||
sut.onConfigInit({ newConfig: defaults });
|
||||
});
|
||||
|
@ -107,31 +91,31 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
describe('handleMigrationSingle', () => {
|
||||
it('should skip when storage template is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: false } });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ storageTemplate: { enabled: false } });
|
||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
||||
expect(storageMock.checkFileExists).not.toHaveBeenCalled();
|
||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(moveMock.create).not.toHaveBeenCalled();
|
||||
expect(moveMock.update).not.toHaveBeenCalled();
|
||||
expect(storageMock.stat).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getByIds).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.checkFileExists).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.copyFile).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.move.create).not.toHaveBeenCalled();
|
||||
expect(mocks.move.update).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.stat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should migrate single moving picture', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const newMotionPicturePath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${assetStub.livePhotoStillAsset.id}.mp4`;
|
||||
const newStillPicturePath = `upload/library/${userStub.user1.id}/2022/2022-06-19/${assetStub.livePhotoStillAsset.id}.jpeg`;
|
||||
|
||||
assetMock.getByIds.mockImplementation((ids) => {
|
||||
mocks.asset.getByIds.mockImplementation((ids) => {
|
||||
const assets = [assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset];
|
||||
return Promise.resolve(
|
||||
ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset),
|
||||
) as Promise<AssetEntity[]>;
|
||||
});
|
||||
|
||||
moveMock.create.mockResolvedValueOnce({
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
entityId: assetStub.livePhotoStillAsset.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -139,7 +123,7 @@ describe(StorageTemplateService.name, () => {
|
|||
newPath: newStillPicturePath,
|
||||
});
|
||||
|
||||
moveMock.create.mockResolvedValueOnce({
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '124',
|
||||
entityId: assetStub.livePhotoMotionAsset.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -151,14 +135,14 @@ describe(StorageTemplateService.name, () => {
|
|||
JobStatus.SUCCESS,
|
||||
);
|
||||
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2);
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
originalPath: newStillPicturePath,
|
||||
});
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
originalPath: newMotionPicturePath,
|
||||
});
|
||||
|
@ -173,13 +157,13 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
userMock.get.mockResolvedValue(user);
|
||||
assetMock.getByIds.mockResolvedValueOnce([asset]);
|
||||
albumMock.getByAssetId.mockResolvedValueOnce([album]);
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.asset.getByIds.mockResolvedValueOnce([asset]);
|
||||
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
|
||||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(moveMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: asset.id,
|
||||
newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`,
|
||||
oldPath: asset.originalPath,
|
||||
|
@ -194,13 +178,13 @@ describe(StorageTemplateService.name, () => {
|
|||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}';
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
userMock.get.mockResolvedValue(user);
|
||||
assetMock.getByIds.mockResolvedValueOnce([asset]);
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.asset.getByIds.mockResolvedValueOnce([asset]);
|
||||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
|
||||
|
||||
const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0');
|
||||
expect(moveMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: asset.id,
|
||||
newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`,
|
||||
oldPath: asset.originalPath,
|
||||
|
@ -209,20 +193,22 @@ describe(StorageTemplateService.name, () => {
|
|||
});
|
||||
|
||||
it('should migrate previously failed move from original path when it still exists', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`;
|
||||
const newPath = `upload/library/${userStub.user1.id}/2023/2023-02-23/${assetStub.image.id}.jpg`;
|
||||
|
||||
storageMock.checkFileExists.mockImplementation((path) => Promise.resolve(path === assetStub.image.originalPath));
|
||||
moveMock.getByEntity.mockResolvedValue({
|
||||
mocks.storage.checkFileExists.mockImplementation((path) =>
|
||||
Promise.resolve(path === assetStub.image.originalPath),
|
||||
);
|
||||
mocks.move.getByEntity.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
oldPath: assetStub.image.originalPath,
|
||||
newPath: previousFailedNewPath,
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
moveMock.update.mockResolvedValue({
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mocks.move.update.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -232,37 +218,37 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
||||
expect(storageMock.rename).toHaveBeenCalledWith(assetStub.image.originalPath, newPath);
|
||||
expect(moveMock.update).toHaveBeenCalledWith('123', {
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(assetStub.image.originalPath, newPath);
|
||||
expect(mocks.move.update).toHaveBeenCalledWith('123', {
|
||||
id: '123',
|
||||
oldPath: assetStub.image.originalPath,
|
||||
newPath,
|
||||
});
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
originalPath: newPath,
|
||||
});
|
||||
});
|
||||
|
||||
it('should migrate previously failed move from previous new path when old path no longer exists, should validate file size still matches before moving', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`;
|
||||
const newPath = `upload/library/${userStub.user1.id}/2023/2023-02-23/${assetStub.image.id}.jpg`;
|
||||
|
||||
storageMock.checkFileExists.mockImplementation((path) => Promise.resolve(path === previousFailedNewPath));
|
||||
storageMock.stat.mockResolvedValue({ size: 5000 } as Stats);
|
||||
cryptoMock.hashFile.mockResolvedValue(assetStub.image.checksum);
|
||||
moveMock.getByEntity.mockResolvedValue({
|
||||
mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === previousFailedNewPath));
|
||||
mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats);
|
||||
mocks.crypto.hashFile.mockResolvedValue(assetStub.image.checksum);
|
||||
mocks.move.getByEntity.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
oldPath: assetStub.image.originalPath,
|
||||
newPath: previousFailedNewPath,
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
moveMock.update.mockResolvedValue({
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mocks.move.update.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -272,31 +258,31 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
||||
expect(storageMock.stat).toHaveBeenCalledWith(previousFailedNewPath);
|
||||
expect(storageMock.rename).toHaveBeenCalledWith(previousFailedNewPath, newPath);
|
||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||
expect(moveMock.update).toHaveBeenCalledWith('123', {
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.storage.stat).toHaveBeenCalledWith(previousFailedNewPath);
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(previousFailedNewPath, newPath);
|
||||
expect(mocks.storage.copyFile).not.toHaveBeenCalled();
|
||||
expect(mocks.move.update).toHaveBeenCalledWith('123', {
|
||||
id: '123',
|
||||
oldPath: previousFailedNewPath,
|
||||
newPath,
|
||||
});
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
originalPath: newPath,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail move if copying and hash of asset and the new file do not match', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const newPath = `upload/library/${userStub.user1.id}/2023/2023-02-23/${assetStub.image.id}.jpg`;
|
||||
|
||||
storageMock.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
storageMock.stat.mockResolvedValue({ size: 5000 } as Stats);
|
||||
cryptoMock.hashFile.mockResolvedValue(Buffer.from('different-hash', 'utf8'));
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
moveMock.create.mockResolvedValue({
|
||||
mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats);
|
||||
mocks.crypto.hashFile.mockResolvedValue(Buffer.from('different-hash', 'utf8'));
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -306,20 +292,20 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(1);
|
||||
expect(storageMock.stat).toHaveBeenCalledWith(newPath);
|
||||
expect(moveMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.storage.stat).toHaveBeenCalledWith(newPath);
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
oldPath: assetStub.image.originalPath,
|
||||
newPath,
|
||||
});
|
||||
expect(storageMock.rename).toHaveBeenCalledWith(assetStub.image.originalPath, newPath);
|
||||
expect(storageMock.copyFile).toHaveBeenCalledWith(assetStub.image.originalPath, newPath);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(newPath);
|
||||
expect(storageMock.unlink).toHaveBeenCalledTimes(1);
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(assetStub.image.originalPath, newPath);
|
||||
expect(mocks.storage.copyFile).toHaveBeenCalledWith(assetStub.image.originalPath, newPath);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(newPath);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -329,22 +315,22 @@ describe(StorageTemplateService.name, () => {
|
|||
`(
|
||||
'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails',
|
||||
async ({ failedPathChecksum, failedPathSize }) => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`;
|
||||
const newPath = `upload/library/${userStub.user1.id}/2023/2023-02-23/${assetStub.image.id}.jpg`;
|
||||
|
||||
storageMock.checkFileExists.mockImplementation((path) => Promise.resolve(previousFailedNewPath === path));
|
||||
storageMock.stat.mockResolvedValue({ size: failedPathSize } as Stats);
|
||||
cryptoMock.hashFile.mockResolvedValue(failedPathChecksum);
|
||||
moveMock.getByEntity.mockResolvedValue({
|
||||
mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(previousFailedNewPath === path));
|
||||
mocks.storage.stat.mockResolvedValue({ size: failedPathSize } as Stats);
|
||||
mocks.crypto.hashFile.mockResolvedValue(failedPathChecksum);
|
||||
mocks.move.getByEntity.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
oldPath: assetStub.image.originalPath,
|
||||
newPath: previousFailedNewPath,
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
moveMock.update.mockResolvedValue({
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mocks.move.update.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -354,37 +340,37 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
||||
expect(storageMock.stat).toHaveBeenCalledWith(previousFailedNewPath);
|
||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||
expect(moveMock.update).not.toHaveBeenCalled();
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.storage.stat).toHaveBeenCalledWith(previousFailedNewPath);
|
||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.copyFile).not.toHaveBeenCalled();
|
||||
expect(mocks.move.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('handle template migration', () => {
|
||||
it('should handle no assets', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
userMock.getList.mockResolvedValue([]);
|
||||
mocks.user.getList.mockResolvedValue([]);
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle an asset with a duplicate destination', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
moveMock.create.mockResolvedValue({
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -392,22 +378,22 @@ describe(StorageTemplateService.name, () => {
|
|||
newPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg',
|
||||
});
|
||||
|
||||
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
||||
storageMock.checkFileExists.mockResolvedValueOnce(false);
|
||||
mocks.storage.checkFileExists.mockResolvedValueOnce(true);
|
||||
mocks.storage.checkFileExists.mockResolvedValueOnce(false);
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2);
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg',
|
||||
});
|
||||
expect(userMock.getList).toHaveBeenCalled();
|
||||
expect(mocks.user.getList).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip when an asset already matches the template', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
...assetStub.image,
|
||||
|
@ -416,19 +402,19 @@ describe(StorageTemplateService.name, () => {
|
|||
],
|
||||
hasNextPage: false,
|
||||
});
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||
expect(storageMock.checkFileExists).not.toHaveBeenCalledTimes(2);
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.copyFile).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.checkFileExists).not.toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip when an asset is probably a duplicate', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
...assetStub.image,
|
||||
|
@ -437,24 +423,24 @@ describe(StorageTemplateService.name, () => {
|
|||
],
|
||||
hasNextPage: false,
|
||||
});
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||
expect(storageMock.copyFile).not.toHaveBeenCalled();
|
||||
expect(storageMock.checkFileExists).not.toHaveBeenCalledTimes(2);
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.copyFile).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.checkFileExists).not.toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should move an asset', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
moveMock.create.mockResolvedValue({
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -464,24 +450,24 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(storageMock.rename).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||
);
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the user storage label', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
userMock.getList.mockResolvedValue([userStub.storageLabel]);
|
||||
moveMock.create.mockResolvedValue({
|
||||
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
|
@ -491,12 +477,12 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(storageMock.rename).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
'upload/library/label-1/2023/2023-02-23/asset-id.jpg',
|
||||
);
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
originalPath: 'upload/library/label-1/2023/2023-02-23/asset-id.jpg',
|
||||
});
|
||||
|
@ -504,105 +490,105 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
it('should copy the file if rename fails due to EXDEV (rename across filesystems)', async () => {
|
||||
const newPath = 'upload/library/user-id/2023/2023-02-23/asset-id.jpg';
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
storageMock.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
moveMock.create.mockResolvedValue({
|
||||
mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
oldPath: assetStub.image.originalPath,
|
||||
newPath,
|
||||
});
|
||||
storageMock.stat.mockResolvedValueOnce({
|
||||
mocks.storage.stat.mockResolvedValueOnce({
|
||||
atime: new Date(),
|
||||
mtime: new Date(),
|
||||
} as Stats);
|
||||
storageMock.stat.mockResolvedValueOnce({
|
||||
mocks.storage.stat.mockResolvedValueOnce({
|
||||
size: 5000,
|
||||
} as Stats);
|
||||
storageMock.stat.mockResolvedValueOnce({
|
||||
mocks.storage.stat.mockResolvedValueOnce({
|
||||
atime: new Date(),
|
||||
mtime: new Date(),
|
||||
} as Stats);
|
||||
cryptoMock.hashFile.mockResolvedValue(assetStub.image.checksum);
|
||||
mocks.crypto.hashFile.mockResolvedValue(assetStub.image.checksum);
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(storageMock.rename).toHaveBeenCalledWith('/original/path.jpg', newPath);
|
||||
expect(storageMock.copyFile).toHaveBeenCalledWith('/original/path.jpg', newPath);
|
||||
expect(storageMock.stat).toHaveBeenCalledWith(newPath);
|
||||
expect(storageMock.stat).toHaveBeenCalledWith(assetStub.image.originalPath);
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(newPath, expect.any(Date), expect.any(Date));
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(assetStub.image.originalPath);
|
||||
expect(storageMock.unlink).toHaveBeenCalledTimes(1);
|
||||
expect(assetMock.update).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith('/original/path.jpg', newPath);
|
||||
expect(mocks.storage.copyFile).toHaveBeenCalledWith('/original/path.jpg', newPath);
|
||||
expect(mocks.storage.stat).toHaveBeenCalledWith(newPath);
|
||||
expect(mocks.storage.stat).toHaveBeenCalledWith(assetStub.image.originalPath);
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(newPath, expect.any(Date), expect.any(Date));
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(assetStub.image.originalPath);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
originalPath: newPath,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update the database if the move fails due to incorrect newPath filesize', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
storageMock.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
moveMock.create.mockResolvedValue({
|
||||
mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
oldPath: assetStub.image.originalPath,
|
||||
newPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||
});
|
||||
storageMock.stat.mockResolvedValue({
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 100,
|
||||
} as Stats);
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(storageMock.rename).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||
);
|
||||
expect(storageMock.copyFile).toHaveBeenCalledWith(
|
||||
expect(mocks.storage.copyFile).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||
);
|
||||
expect(storageMock.stat).toHaveBeenCalledWith('upload/library/user-id/2023/2023-02-23/asset-id.jpg');
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.stat).toHaveBeenCalledWith('upload/library/user-id/2023/2023-02-23/asset-id.jpg');
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update the database if the move fails', async () => {
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
mocks.asset.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
storageMock.rename.mockRejectedValue(new Error('Read only system'));
|
||||
storageMock.copyFile.mockRejectedValue(new Error('Read only system'));
|
||||
moveMock.create.mockResolvedValue({
|
||||
mocks.storage.rename.mockRejectedValue(new Error('Read only system'));
|
||||
mocks.storage.copyFile.mockRejectedValue(new Error('Read only system'));
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: 'move-123',
|
||||
entityId: '123',
|
||||
pathType: AssetPathType.ORIGINAL,
|
||||
oldPath: assetStub.image.originalPath,
|
||||
newPath: '',
|
||||
});
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(assetMock.getAll).toHaveBeenCalled();
|
||||
expect(storageMock.rename).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||
);
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { StorageService } from 'src/services/storage.service';
|
||||
import { IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { ImmichStartupError } from 'src/utils/misc';
|
||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(StorageService.name, () => {
|
||||
let sut: StorageService;
|
||||
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let loggerMock: Mocked<ILoggingRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, configMock, loggerMock, storageMock, systemMock } = newTestService(StorageService));
|
||||
({ sut, mocks } = newTestService(StorageService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -25,11 +18,11 @@ describe(StorageService.name, () => {
|
|||
|
||||
describe('onBootstrap', () => {
|
||||
it('should enable mount folder checking', async () => {
|
||||
systemMock.get.mockResolvedValue(null);
|
||||
mocks.systemMetadata.get.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, {
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, {
|
||||
mountChecks: {
|
||||
backups: true,
|
||||
'encoded-video': true,
|
||||
|
@ -39,22 +32,22 @@ describe(StorageService.name, () => {
|
|||
upload: true,
|
||||
},
|
||||
});
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/encoded-video');
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library');
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile');
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs');
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/upload');
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/backups');
|
||||
expect(storageMock.createFile).toHaveBeenCalledWith('upload/encoded-video/.immich', expect.any(Buffer));
|
||||
expect(storageMock.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer));
|
||||
expect(storageMock.createFile).toHaveBeenCalledWith('upload/profile/.immich', expect.any(Buffer));
|
||||
expect(storageMock.createFile).toHaveBeenCalledWith('upload/thumbs/.immich', expect.any(Buffer));
|
||||
expect(storageMock.createFile).toHaveBeenCalledWith('upload/upload/.immich', expect.any(Buffer));
|
||||
expect(storageMock.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer));
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/encoded-video');
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/library');
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/profile');
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs');
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/upload');
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/backups');
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/encoded-video/.immich', expect.any(Buffer));
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer));
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/profile/.immich', expect.any(Buffer));
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/thumbs/.immich', expect.any(Buffer));
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/upload/.immich', expect.any(Buffer));
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer));
|
||||
});
|
||||
|
||||
it('should enable mount folder checking for a new folder type', async () => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
mountChecks: {
|
||||
backups: false,
|
||||
'encoded-video': true,
|
||||
|
@ -67,7 +60,7 @@ describe(StorageService.name, () => {
|
|||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, {
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, {
|
||||
mountChecks: {
|
||||
backups: true,
|
||||
'encoded-video': true,
|
||||
|
@ -77,64 +70,68 @@ describe(StorageService.name, () => {
|
|||
upload: true,
|
||||
},
|
||||
});
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledTimes(2);
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library');
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/backups');
|
||||
expect(storageMock.createFile).toHaveBeenCalledTimes(2);
|
||||
expect(storageMock.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer));
|
||||
expect(storageMock.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer));
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/library');
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/backups');
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer));
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer));
|
||||
});
|
||||
|
||||
it('should throw an error if .immich is missing', async () => {
|
||||
systemMock.get.mockResolvedValue({ mountChecks: { upload: true } });
|
||||
storageMock.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } });
|
||||
mocks.storage.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to read');
|
||||
|
||||
expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled();
|
||||
expect(systemMock.set).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if .immich is present but read-only', async () => {
|
||||
systemMock.get.mockResolvedValue({ mountChecks: { upload: true } });
|
||||
storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } });
|
||||
mocks.storage.overwriteFile.mockRejectedValue(
|
||||
new Error("ENOENT: no such file or directory, open '/app/.immich'"),
|
||||
);
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to write');
|
||||
|
||||
expect(systemMock.set).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip mount file creation if file already exists', async () => {
|
||||
const error = new Error('Error creating file') as any;
|
||||
error.code = 'EEXIST';
|
||||
systemMock.get.mockResolvedValue({ mountChecks: {} });
|
||||
storageMock.createFile.mockRejectedValue(error);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} });
|
||||
mocks.storage.createFile.mockRejectedValue(error);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(loggerMock.warn).toHaveBeenCalledWith('Found existing mount file, skipping creation');
|
||||
expect(mocks.logger.warn).toHaveBeenCalledWith('Found existing mount file, skipping creation');
|
||||
});
|
||||
|
||||
it('should throw an error if mount file could not be created', async () => {
|
||||
systemMock.get.mockResolvedValue({ mountChecks: {} });
|
||||
storageMock.createFile.mockRejectedValue(new Error('Error creating file'));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} });
|
||||
mocks.storage.createFile.mockRejectedValue(new Error('Error creating file'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toBeInstanceOf(ImmichStartupError);
|
||||
expect(systemMock.set).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should startup if checks are disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({ mountChecks: { upload: true } });
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } });
|
||||
mocks.config.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
storage: { ignoreMountCheckErrors: true },
|
||||
}),
|
||||
);
|
||||
storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||
mocks.storage.overwriteFile.mockRejectedValue(
|
||||
new Error("ENOENT: no such file or directory, open '/app/.immich'"),
|
||||
);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(systemMock.set).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -142,21 +139,21 @@ describe(StorageService.name, () => {
|
|||
it('should handle null values', async () => {
|
||||
await sut.handleDeleteFiles({ files: [undefined, null] });
|
||||
|
||||
expect(storageMock.unlink).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.unlink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle an error removing a file', async () => {
|
||||
storageMock.unlink.mockRejectedValue(new Error('something-went-wrong'));
|
||||
mocks.storage.unlink.mockRejectedValue(new Error('something-went-wrong'));
|
||||
|
||||
await sut.handleDeleteFiles({ files: ['path/to/something'] });
|
||||
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith('path/to/something');
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith('path/to/something');
|
||||
});
|
||||
|
||||
it('should remove the file', async () => {
|
||||
await sut.handleDeleteFiles({ files: ['path/to/something'] });
|
||||
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith('path/to/something');
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith('path/to/something');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,27 +1,20 @@
|
|||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
import { IAuditRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const untilDate = new Date(2024);
|
||||
const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: true };
|
||||
|
||||
describe(SyncService.name, () => {
|
||||
let sut: SyncService;
|
||||
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let auditMock: Mocked<IAuditRepository>;
|
||||
let partnerMock: Mocked<IPartnerRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, assetMock, auditMock, partnerMock } = newTestService(SyncService));
|
||||
({ sut, mocks } = newTestService(SyncService));
|
||||
});
|
||||
|
||||
it('should exist', () => {
|
||||
|
@ -30,12 +23,12 @@ describe(SyncService.name, () => {
|
|||
|
||||
describe('getAllAssetsForUserFullSync', () => {
|
||||
it('should return a list of all assets owned by the user', async () => {
|
||||
assetMock.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]);
|
||||
mocks.asset.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]);
|
||||
await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([
|
||||
mapAsset(assetStub.external, mapAssetOpts),
|
||||
mapAsset(assetStub.hasEncodedVideo, mapAssetOpts),
|
||||
]);
|
||||
expect(assetMock.getAllForUserFullSync).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({
|
||||
ownerId: authStub.user1.user.id,
|
||||
updatedUntil: untilDate,
|
||||
limit: 2,
|
||||
|
@ -45,39 +38,39 @@ describe(SyncService.name, () => {
|
|||
|
||||
describe('getChangesForDeltaSync', () => {
|
||||
it('should return a response requiring a full sync when partners are out of sync', async () => {
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]);
|
||||
mocks.partner.getAll.mockResolvedValue([partnerStub.adminToUser1]);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
|
||||
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0);
|
||||
expect(auditMock.getAfter).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should return a response requiring a full sync when last sync was too long ago', async () => {
|
||||
partnerMock.getAll.mockResolvedValue([]);
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
|
||||
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0);
|
||||
expect(auditMock.getAfter).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should return a response requiring a full sync when there are too many changes', async () => {
|
||||
partnerMock.getAll.mockResolvedValue([]);
|
||||
assetMock.getChangedDeltaSync.mockResolvedValue(
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue(
|
||||
Array.from<AssetEntity>({ length: 10_000 }).fill(assetStub.image),
|
||||
);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
|
||||
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(1);
|
||||
expect(auditMock.getAfter).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should return a response with changes and deletions', async () => {
|
||||
partnerMock.getAll.mockResolvedValue([]);
|
||||
assetMock.getChangedDeltaSync.mockResolvedValue([assetStub.image1]);
|
||||
auditMock.getAfter.mockResolvedValue([assetStub.external.id]);
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue([assetStub.image1]);
|
||||
mocks.audit.getAfter.mockResolvedValue([assetStub.external.id]);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({
|
||||
|
@ -85,8 +78,8 @@ describe(SyncService.name, () => {
|
|||
upserted: [mapAsset(assetStub.image1, mapAssetOpts)],
|
||||
deleted: [assetStub.external.id],
|
||||
});
|
||||
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(1);
|
||||
expect(auditMock.getAfter).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,13 +12,11 @@ import {
|
|||
VideoCodec,
|
||||
VideoContainer,
|
||||
} from 'src/enum';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { SystemConfigService } from 'src/services/system-config.service';
|
||||
import { DeepPartial, IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { DeepPartial } from 'src/types';
|
||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const partialConfig = {
|
||||
ffmpeg: { crf: 30 },
|
||||
|
@ -198,14 +196,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
|
||||
describe(SystemConfigService.name, () => {
|
||||
let sut: SystemConfigService;
|
||||
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let loggerMock: Mocked<ILoggingRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, configMock, eventMock, loggerMock, systemMock } = newTestService(SystemConfigService));
|
||||
({ sut, mocks } = newTestService(SystemConfigService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -214,22 +208,22 @@ describe(SystemConfigService.name, () => {
|
|||
|
||||
describe('getDefaults', () => {
|
||||
it('should return the default config', () => {
|
||||
systemMock.get.mockResolvedValue(partialConfig);
|
||||
mocks.systemMetadata.get.mockResolvedValue(partialConfig);
|
||||
|
||||
expect(sut.getDefaults()).toEqual(defaults);
|
||||
expect(systemMock.get).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('should return the default config', async () => {
|
||||
systemMock.get.mockResolvedValue({});
|
||||
mocks.systemMetadata.get.mockResolvedValue({});
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
|
||||
});
|
||||
|
||||
it('should merge the overrides', async () => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
ffmpeg: { crf: 30 },
|
||||
oauth: { autoLaunch: true },
|
||||
trash: { days: 10 },
|
||||
|
@ -240,17 +234,17 @@ describe(SystemConfigService.name, () => {
|
|||
});
|
||||
|
||||
it('should load the config from a json file', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
|
||||
|
||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
});
|
||||
|
||||
it('should transform booleans', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { twoPass: 'false' } }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { twoPass: 'false' } }));
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toMatchObject({
|
||||
ffmpeg: expect.objectContaining({ twoPass: false }),
|
||||
|
@ -258,8 +252,8 @@ describe(SystemConfigService.name, () => {
|
|||
});
|
||||
|
||||
it('should transform numbers', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { threads: '42' } }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { threads: '42' } }));
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toMatchObject({
|
||||
ffmpeg: expect.objectContaining({ threads: 42 }),
|
||||
|
@ -267,8 +261,10 @@ describe(SystemConfigService.name, () => {
|
|||
});
|
||||
|
||||
it('should accept valid cron expressions', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: '0 0 * * *' } } }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(
|
||||
JSON.stringify({ library: { scan: { cronExpression: '0 0 * * *' } } }),
|
||||
);
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toMatchObject({
|
||||
library: {
|
||||
|
@ -281,8 +277,8 @@ describe(SystemConfigService.name, () => {
|
|||
});
|
||||
|
||||
it('should reject invalid cron expressions', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } }));
|
||||
|
||||
await expect(sut.getSystemConfig()).rejects.toThrow(
|
||||
'library.scan.cronExpression has failed the following constraints: cronValidator',
|
||||
|
@ -290,22 +286,22 @@ describe(SystemConfigService.name, () => {
|
|||
});
|
||||
|
||||
it('should log errors with the config file', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
|
||||
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
|
||||
|
||||
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
|
||||
|
||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
expect(loggerMock.error).toHaveBeenCalledTimes(2);
|
||||
expect(loggerMock.error.mock.calls[0][0]).toEqual('Unable to load configuration file: immich-config.json');
|
||||
expect(loggerMock.error.mock.calls[1][0].toString()).toEqual(
|
||||
expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
expect(mocks.logger.error).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.logger.error.mock.calls[0][0]).toEqual('Unable to load configuration file: immich-config.json');
|
||||
expect(mocks.logger.error.mock.calls[1][0].toString()).toEqual(
|
||||
expect.stringContaining('YAMLException: duplicated mapping key (1:20)'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should load the config from a yaml file', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||
const partialConfig = `
|
||||
ffmpeg:
|
||||
crf: 30
|
||||
|
@ -316,26 +312,26 @@ describe(SystemConfigService.name, () => {
|
|||
user:
|
||||
deleteDelay: 15
|
||||
`;
|
||||
systemMock.readFile.mockResolvedValue(partialConfig);
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(partialConfig);
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
|
||||
|
||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
|
||||
expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.yaml');
|
||||
});
|
||||
|
||||
it('should accept an empty configuration file', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
|
||||
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
|
||||
|
||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
});
|
||||
|
||||
it('should allow underscores in the machine learning url', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
const partialConfig = { machineLearning: { urls: ['immich_machine_learning'] } };
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
const config = await sut.getSystemConfig();
|
||||
expect(config.machineLearning.urls).toEqual(['immich_machine_learning']);
|
||||
|
@ -349,9 +345,9 @@ describe(SystemConfigService.name, () => {
|
|||
|
||||
for (const { should, externalDomain, result } of externalDomainTests) {
|
||||
it(`should normalize an external domain ${should}`, async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
const partialConfig = { server: { externalDomain } };
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
const config = await sut.getSystemConfig();
|
||||
expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app');
|
||||
|
@ -359,14 +355,14 @@ describe(SystemConfigService.name, () => {
|
|||
}
|
||||
|
||||
it('should warn for unknown options in yaml', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||
const partialConfig = `
|
||||
unknownOption: true
|
||||
`;
|
||||
systemMock.readFile.mockResolvedValue(partialConfig);
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(partialConfig);
|
||||
|
||||
await sut.getSystemConfig();
|
||||
expect(loggerMock.warn).toHaveBeenCalled();
|
||||
expect(mocks.logger.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const tests = [
|
||||
|
@ -380,12 +376,12 @@ describe(SystemConfigService.name, () => {
|
|||
|
||||
for (const test of tests) {
|
||||
it(`should ${test.should}`, async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(test.config));
|
||||
|
||||
if (test.warn) {
|
||||
await sut.getSystemConfig();
|
||||
expect(loggerMock.warn).toHaveBeenCalled();
|
||||
expect(mocks.logger.warn).toHaveBeenCalled();
|
||||
} else {
|
||||
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
|
||||
}
|
||||
|
@ -395,19 +391,19 @@ describe(SystemConfigService.name, () => {
|
|||
|
||||
describe('updateConfig', () => {
|
||||
it('should update the config and emit an event', async () => {
|
||||
systemMock.get.mockResolvedValue(partialConfig);
|
||||
mocks.systemMetadata.get.mockResolvedValue(partialConfig);
|
||||
await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig);
|
||||
expect(eventMock.emit).toHaveBeenCalledWith(
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith(
|
||||
'config.update',
|
||||
expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if a config file is in use', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(systemMock.set).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
||||
import { ISystemMetadataRepository } from 'src/types';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(SystemMetadataService.name, () => {
|
||||
let sut: SystemMetadataService;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, systemMock } = newTestService(SystemMetadataService));
|
||||
({ sut, mocks } = newTestService(SystemMetadataService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -18,32 +16,32 @@ describe(SystemMetadataService.name, () => {
|
|||
|
||||
describe('getAdminOnboarding', () => {
|
||||
it('should get isOnboarded state', async () => {
|
||||
systemMock.get.mockResolvedValue({ isOnboarded: true });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ isOnboarded: true });
|
||||
await expect(sut.getAdminOnboarding()).resolves.toEqual({ isOnboarded: true });
|
||||
expect(systemMock.get).toHaveBeenCalledWith('admin-onboarding');
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalledWith('admin-onboarding');
|
||||
});
|
||||
|
||||
it('should default isOnboarded to false', async () => {
|
||||
await expect(sut.getAdminOnboarding()).resolves.toEqual({ isOnboarded: false });
|
||||
expect(systemMock.get).toHaveBeenCalledWith('admin-onboarding');
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalledWith('admin-onboarding');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAdminOnboarding', () => {
|
||||
it('should update isOnboarded to true', async () => {
|
||||
await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined();
|
||||
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
|
||||
});
|
||||
|
||||
it('should update isOnboarded to false', async () => {
|
||||
await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined();
|
||||
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false });
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReverseGeocodingState', () => {
|
||||
it('should get reverse geocoding state', async () => {
|
||||
systemMock.get.mockResolvedValue({ lastUpdate: '2024-01-01', lastImportFileName: 'foo.bar' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ lastUpdate: '2024-01-01', lastImportFileName: 'foo.bar' });
|
||||
await expect(sut.getReverseGeocodingState()).resolves.toEqual({
|
||||
lastUpdate: '2024-01-01',
|
||||
lastImportFileName: 'foo.bar',
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||
import { TagService } from 'src/services/tag.service';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(TagService.name, () => {
|
||||
let sut: TagService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let tagMock: Mocked<ITagRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, tagMock } = newTestService(TagService));
|
||||
({ sut, mocks } = newTestService(TagService));
|
||||
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1']));
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1']));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -27,76 +22,76 @@ describe(TagService.name, () => {
|
|||
|
||||
describe('getAll', () => {
|
||||
it('should return all tags for a user', async () => {
|
||||
tagMock.getAll.mockResolvedValue([tagStub.tag1]);
|
||||
mocks.tag.getAll.mockResolvedValue([tagStub.tag1]);
|
||||
await expect(sut.getAll(authStub.admin)).resolves.toEqual([tagResponseStub.tag1]);
|
||||
expect(tagMock.getAll).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(mocks.tag.getAll).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should throw an error for an invalid id', async () => {
|
||||
tagMock.get.mockResolvedValue(null);
|
||||
mocks.tag.get.mockResolvedValue(null);
|
||||
await expect(sut.get(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(tagMock.get).toHaveBeenCalledWith('tag-1');
|
||||
expect(mocks.tag.get).toHaveBeenCalledWith('tag-1');
|
||||
});
|
||||
|
||||
it('should return a tag for a user', async () => {
|
||||
tagMock.get.mockResolvedValue(tagStub.tag1);
|
||||
mocks.tag.get.mockResolvedValue(tagStub.tag1);
|
||||
await expect(sut.get(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1);
|
||||
expect(tagMock.get).toHaveBeenCalledWith('tag-1');
|
||||
expect(mocks.tag.get).toHaveBeenCalledWith('tag-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should throw an error for no parent tag access', async () => {
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
await expect(sut.create(authStub.admin, { name: 'tag', parentId: 'tag-parent' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(tagMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a tag with a parent', async () => {
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent']));
|
||||
tagMock.create.mockResolvedValue(tagStub.tag1);
|
||||
tagMock.get.mockResolvedValueOnce(tagStub.parent);
|
||||
tagMock.get.mockResolvedValueOnce(tagStub.child);
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent']));
|
||||
mocks.tag.create.mockResolvedValue(tagStub.tag1);
|
||||
mocks.tag.get.mockResolvedValueOnce(tagStub.parent);
|
||||
mocks.tag.get.mockResolvedValueOnce(tagStub.child);
|
||||
await expect(sut.create(authStub.admin, { name: 'tagA', parentId: 'tag-parent' })).resolves.toBeDefined();
|
||||
expect(tagMock.create).toHaveBeenCalledWith(expect.objectContaining({ value: 'Parent/tagA' }));
|
||||
expect(mocks.tag.create).toHaveBeenCalledWith(expect.objectContaining({ value: 'Parent/tagA' }));
|
||||
});
|
||||
|
||||
it('should handle invalid parent ids', async () => {
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent']));
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent']));
|
||||
await expect(sut.create(authStub.admin, { name: 'tagA', parentId: 'tag-parent' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(tagMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should throw an error for a duplicate tag', async () => {
|
||||
tagMock.getByValue.mockResolvedValue(tagStub.tag1);
|
||||
mocks.tag.getByValue.mockResolvedValue(tagStub.tag1);
|
||||
await expect(sut.create(authStub.admin, { name: 'tag-1' })).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(tagMock.getByValue).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
|
||||
expect(tagMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.getByValue).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1');
|
||||
expect(mocks.tag.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a new tag', async () => {
|
||||
tagMock.create.mockResolvedValue(tagStub.tag1);
|
||||
mocks.tag.create.mockResolvedValue(tagStub.tag1);
|
||||
await expect(sut.create(authStub.admin, { name: 'tag-1' })).resolves.toEqual(tagResponseStub.tag1);
|
||||
expect(tagMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.tag.create).toHaveBeenCalledWith({
|
||||
userId: authStub.admin.user.id,
|
||||
value: 'tag-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new tag with optional color', async () => {
|
||||
tagMock.create.mockResolvedValue(tagStub.color1);
|
||||
mocks.tag.create.mockResolvedValue(tagStub.color1);
|
||||
await expect(sut.create(authStub.admin, { name: 'tag-1', color: '#000000' })).resolves.toEqual(
|
||||
tagResponseStub.color1,
|
||||
);
|
||||
expect(tagMock.create).toHaveBeenCalledWith({
|
||||
expect(mocks.tag.create).toHaveBeenCalledWith({
|
||||
userId: authStub.admin.user.id,
|
||||
value: 'tag-1',
|
||||
color: '#000000',
|
||||
|
@ -106,26 +101,26 @@ describe(TagService.name, () => {
|
|||
|
||||
describe('update', () => {
|
||||
it('should throw an error for no update permission', async () => {
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
await expect(sut.update(authStub.admin, 'tag-1', { color: '#000000' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(tagMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update a tag', async () => {
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1']));
|
||||
tagMock.update.mockResolvedValue(tagStub.color1);
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1']));
|
||||
mocks.tag.update.mockResolvedValue(tagStub.color1);
|
||||
await expect(sut.update(authStub.admin, 'tag-1', { color: '#000000' })).resolves.toEqual(tagResponseStub.color1);
|
||||
expect(tagMock.update).toHaveBeenCalledWith({ id: 'tag-1', color: '#000000' });
|
||||
expect(mocks.tag.update).toHaveBeenCalledWith({ id: 'tag-1', color: '#000000' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsert', () => {
|
||||
it('should upsert a new tag', async () => {
|
||||
tagMock.upsertValue.mockResolvedValue(tagStub.parent);
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parent);
|
||||
await expect(sut.upsert(authStub.admin, { tags: ['Parent'] })).resolves.toBeDefined();
|
||||
expect(tagMock.upsertValue).toHaveBeenCalledWith({
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({
|
||||
value: 'Parent',
|
||||
userId: 'admin_id',
|
||||
parentId: undefined,
|
||||
|
@ -133,16 +128,16 @@ describe(TagService.name, () => {
|
|||
});
|
||||
|
||||
it('should upsert a nested tag', async () => {
|
||||
tagMock.getByValue.mockResolvedValueOnce(null);
|
||||
tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent);
|
||||
tagMock.upsertValue.mockResolvedValueOnce(tagStub.child);
|
||||
mocks.tag.getByValue.mockResolvedValueOnce(null);
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parent);
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.child);
|
||||
await expect(sut.upsert(authStub.admin, { tags: ['Parent/Child'] })).resolves.toBeDefined();
|
||||
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||
value: 'Parent',
|
||||
userId: 'admin_id',
|
||||
parent: undefined,
|
||||
});
|
||||
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||
value: 'Parent/Child',
|
||||
userId: 'admin_id',
|
||||
parent: expect.objectContaining({ id: 'tag-parent' }),
|
||||
|
@ -150,16 +145,16 @@ describe(TagService.name, () => {
|
|||
});
|
||||
|
||||
it('should upsert a tag and ignore leading and trailing slashes', async () => {
|
||||
tagMock.getByValue.mockResolvedValueOnce(null);
|
||||
tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent);
|
||||
tagMock.upsertValue.mockResolvedValueOnce(tagStub.child);
|
||||
mocks.tag.getByValue.mockResolvedValueOnce(null);
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parent);
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.child);
|
||||
await expect(sut.upsert(authStub.admin, { tags: ['/Parent/Child/'] })).resolves.toBeDefined();
|
||||
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||
value: 'Parent',
|
||||
userId: 'admin_id',
|
||||
parent: undefined,
|
||||
});
|
||||
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||
value: 'Parent/Child',
|
||||
userId: 'admin_id',
|
||||
parent: expect.objectContaining({ id: 'tag-parent' }),
|
||||
|
@ -169,32 +164,32 @@ describe(TagService.name, () => {
|
|||
|
||||
describe('remove', () => {
|
||||
it('should throw an error for an invalid id', async () => {
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(tagMock.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove a tag', async () => {
|
||||
tagMock.get.mockResolvedValue(tagStub.tag1);
|
||||
mocks.tag.get.mockResolvedValue(tagStub.tag1);
|
||||
await sut.remove(authStub.admin, 'tag-1');
|
||||
expect(tagMock.delete).toHaveBeenCalledWith('tag-1');
|
||||
expect(mocks.tag.delete).toHaveBeenCalledWith('tag-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkTagAssets', () => {
|
||||
it('should handle invalid requests', async () => {
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
tagMock.upsertAssetIds.mockResolvedValue([]);
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
mocks.tag.upsertAssetIds.mockResolvedValue([]);
|
||||
await expect(sut.bulkTagAssets(authStub.admin, { tagIds: ['tag-1'], assetIds: ['asset-1'] })).resolves.toEqual({
|
||||
count: 0,
|
||||
});
|
||||
expect(tagMock.upsertAssetIds).toHaveBeenCalledWith([]);
|
||||
expect(mocks.tag.upsertAssetIds).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should upsert records', async () => {
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1', 'tag-2']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
tagMock.upsertAssetIds.mockResolvedValue([
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1', 'tag-2']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
|
||||
mocks.tag.upsertAssetIds.mockResolvedValue([
|
||||
{ tagId: 'tag-1', assetId: 'asset-1' },
|
||||
{ tagId: 'tag-1', assetId: 'asset-2' },
|
||||
{ tagId: 'tag-1', assetId: 'asset-3' },
|
||||
|
@ -207,7 +202,7 @@ describe(TagService.name, () => {
|
|||
).resolves.toEqual({
|
||||
count: 6,
|
||||
});
|
||||
expect(tagMock.upsertAssetIds).toHaveBeenCalledWith([
|
||||
expect(mocks.tag.upsertAssetIds).toHaveBeenCalledWith([
|
||||
{ tagId: 'tag-1', assetId: 'asset-1' },
|
||||
{ tagId: 'tag-1', assetId: 'asset-2' },
|
||||
{ tagId: 'tag-1', assetId: 'asset-3' },
|
||||
|
@ -220,19 +215,19 @@ describe(TagService.name, () => {
|
|||
|
||||
describe('addAssets', () => {
|
||||
it('should handle invalid ids', async () => {
|
||||
tagMock.get.mockResolvedValue(null);
|
||||
tagMock.getAssetIds.mockResolvedValue(new Set([]));
|
||||
mocks.tag.get.mockResolvedValue(null);
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set([]));
|
||||
await expect(sut.addAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([
|
||||
{ id: 'asset-1', success: false, error: 'no_permission' },
|
||||
]);
|
||||
expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']);
|
||||
expect(tagMock.addAssetIds).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']);
|
||||
expect(mocks.tag.addAssetIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should accept accept ids that are new and reject the rest', async () => {
|
||||
tagMock.get.mockResolvedValue(tagStub.tag1);
|
||||
tagMock.getAssetIds.mockResolvedValue(new Set(['asset-1']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2']));
|
||||
mocks.tag.get.mockResolvedValue(tagStub.tag1);
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2']));
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.admin, 'tag-1', {
|
||||
|
@ -243,23 +238,23 @@ describe(TagService.name, () => {
|
|||
{ id: 'asset-2', success: true },
|
||||
]);
|
||||
|
||||
expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(tagMock.addAssetIds).toHaveBeenCalledWith('tag-1', ['asset-2']);
|
||||
expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.addAssetIds).toHaveBeenCalledWith('tag-1', ['asset-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeAssets', () => {
|
||||
it('should throw an error for an invalid id', async () => {
|
||||
tagMock.get.mockResolvedValue(null);
|
||||
tagMock.getAssetIds.mockResolvedValue(new Set());
|
||||
mocks.tag.get.mockResolvedValue(null);
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set());
|
||||
await expect(sut.removeAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([
|
||||
{ id: 'asset-1', success: false, error: 'not_found' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should accept accept ids that are tagged and reject the rest', async () => {
|
||||
tagMock.get.mockResolvedValue(tagStub.tag1);
|
||||
tagMock.getAssetIds.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.tag.get.mockResolvedValue(tagStub.tag1);
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1']));
|
||||
|
||||
await expect(
|
||||
sut.removeAssets(authStub.admin, 'tag-1', {
|
||||
|
@ -270,15 +265,15 @@ describe(TagService.name, () => {
|
|||
{ id: 'asset-2', success: false, error: BulkIdErrorReason.NOT_FOUND },
|
||||
]);
|
||||
|
||||
expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(tagMock.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']);
|
||||
expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleTagCleanup', () => {
|
||||
it('should delete empty tags', async () => {
|
||||
await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.SUCCESS);
|
||||
expect(tagMock.deleteEmptyTags).toHaveBeenCalled();
|
||||
expect(mocks.tag.deleteEmptyTags).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,32 +1,28 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface';
|
||||
import { TimeBucketSize } from 'src/interfaces/asset.interface';
|
||||
import { TimelineService } from 'src/services/timeline.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(TimelineService.name, () => {
|
||||
let sut: TimelineService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, assetMock } = newTestService(TimelineService));
|
||||
({ sut, mocks } = newTestService(TimelineService));
|
||||
});
|
||||
|
||||
describe('getTimeBuckets', () => {
|
||||
it("should return buckets if userId and albumId aren't set", async () => {
|
||||
assetMock.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
||||
mocks.asset.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
||||
|
||||
await expect(
|
||||
sut.getTimeBuckets(authStub.admin, {
|
||||
size: TimeBucketSize.DAY,
|
||||
}),
|
||||
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
|
||||
expect(assetMock.getTimeBuckets).toHaveBeenCalledWith({
|
||||
expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({
|
||||
size: TimeBucketSize.DAY,
|
||||
userIds: [authStub.admin.user.id],
|
||||
});
|
||||
|
@ -35,15 +31,15 @@ describe(TimelineService.name, () => {
|
|||
|
||||
describe('getTimeBucket', () => {
|
||||
it('should return the assets for a album time bucket if user has album.read', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
|
||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
||||
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
albumId: 'album-id',
|
||||
|
@ -51,7 +47,7 @@ describe(TimelineService.name, () => {
|
|||
});
|
||||
|
||||
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
|
@ -61,7 +57,7 @@ describe(TimelineService.name, () => {
|
|||
userId: authStub.admin.user.id,
|
||||
}),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
expect(assetMock.getTimeBucket).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||
'bucket',
|
||||
expect.objectContaining({
|
||||
size: TimeBucketSize.DAY,
|
||||
|
@ -73,7 +69,7 @@ describe(TimelineService.name, () => {
|
|||
});
|
||||
|
||||
it('should include partner shared assets', async () => {
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
|
@ -84,7 +80,7 @@ describe(TimelineService.name, () => {
|
|||
withPartners: true,
|
||||
}),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: false,
|
||||
|
@ -94,8 +90,8 @@ describe(TimelineService.name, () => {
|
|||
});
|
||||
|
||||
it('should check permissions to read tag', async () => {
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-123']));
|
||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-123']));
|
||||
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
|
@ -105,7 +101,7 @@ describe(TimelineService.name, () => {
|
|||
tagId: 'tag-123',
|
||||
}),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
tagId: 'tag-123',
|
||||
timeBucket: 'bucket',
|
||||
|
@ -114,8 +110,8 @@ describe(TimelineService.name, () => {
|
|||
});
|
||||
|
||||
it('should strip metadata if showExif is disabled', async () => {
|
||||
accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-id']));
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-id']));
|
||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
|
||||
const buckets = await sut.getTimeBucket(
|
||||
{ ...authStub.admin, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
|
||||
|
@ -128,7 +124,7 @@ describe(TimelineService.name, () => {
|
|||
);
|
||||
expect(buckets).toEqual([expect.objectContaining({ id: 'asset-id' })]);
|
||||
expect(buckets[0]).not.toHaveProperty('exif');
|
||||
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||
size: TimeBucketSize.DAY,
|
||||
timeBucket: 'bucket',
|
||||
isArchived: true,
|
||||
|
@ -137,7 +133,7 @@ describe(TimelineService.name, () => {
|
|||
});
|
||||
|
||||
it('should return the assets for a library time bucket if user has library.read', async () => {
|
||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
|
@ -146,7 +142,7 @@ describe(TimelineService.name, () => {
|
|||
userId: authStub.admin.user.id,
|
||||
}),
|
||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||
expect(assetMock.getTimeBucket).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||
'bucket',
|
||||
expect.objectContaining({
|
||||
size: TimeBucketSize.DAY,
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { TrashService } from 'src/services/trash.service';
|
||||
import { ITrashRepository } from 'src/types';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
async function* makeAssetIdStream(count: number): AsyncIterableIterator<{ id: string }> {
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
@ -16,17 +13,14 @@ async function* makeAssetIdStream(count: number): AsyncIterableIterator<{ id: st
|
|||
|
||||
describe(TrashService.name, () => {
|
||||
let sut: TrashService;
|
||||
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let trashMock: Mocked<ITrashRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, accessMock, jobMock, trashMock } = newTestService(TrashService));
|
||||
({ sut, mocks } = newTestService(TrashService));
|
||||
});
|
||||
|
||||
describe('restoreAssets', () => {
|
||||
|
@ -40,64 +34,64 @@ describe(TrashService.name, () => {
|
|||
|
||||
it('should handle an empty list', async () => {
|
||||
await expect(sut.restoreAssets(authStub.user1, { ids: [] })).resolves.toEqual({ count: 0 });
|
||||
expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
|
||||
expect(mocks.access.asset.checkOwnerAccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should restore a batch of assets', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
|
||||
|
||||
await sut.restoreAssets(authStub.user1, { ids: ['asset1', 'asset2'] });
|
||||
|
||||
expect(trashMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']);
|
||||
expect(jobMock.queue.mock.calls).toEqual([]);
|
||||
expect(mocks.trash.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']);
|
||||
expect(mocks.job.queue.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restore', () => {
|
||||
it('should handle an empty trash', async () => {
|
||||
trashMock.getDeletedIds.mockResolvedValue(makeAssetIdStream(0));
|
||||
trashMock.restore.mockResolvedValue(0);
|
||||
mocks.trash.getDeletedIds.mockResolvedValue(makeAssetIdStream(0));
|
||||
mocks.trash.restore.mockResolvedValue(0);
|
||||
await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 0 });
|
||||
expect(trashMock.restore).toHaveBeenCalledWith('user-id');
|
||||
expect(mocks.trash.restore).toHaveBeenCalledWith('user-id');
|
||||
});
|
||||
|
||||
it('should restore', async () => {
|
||||
trashMock.getDeletedIds.mockResolvedValue(makeAssetIdStream(1));
|
||||
trashMock.restore.mockResolvedValue(1);
|
||||
mocks.trash.getDeletedIds.mockResolvedValue(makeAssetIdStream(1));
|
||||
mocks.trash.restore.mockResolvedValue(1);
|
||||
await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 1 });
|
||||
expect(trashMock.restore).toHaveBeenCalledWith('user-id');
|
||||
expect(mocks.trash.restore).toHaveBeenCalledWith('user-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty', () => {
|
||||
it('should handle an empty trash', async () => {
|
||||
trashMock.getDeletedIds.mockResolvedValue(makeAssetIdStream(0));
|
||||
trashMock.empty.mockResolvedValue(0);
|
||||
mocks.trash.getDeletedIds.mockResolvedValue(makeAssetIdStream(0));
|
||||
mocks.trash.empty.mockResolvedValue(0);
|
||||
await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 0 });
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should empty the trash', async () => {
|
||||
trashMock.getDeletedIds.mockResolvedValue(makeAssetIdStream(1));
|
||||
trashMock.empty.mockResolvedValue(1);
|
||||
mocks.trash.getDeletedIds.mockResolvedValue(makeAssetIdStream(1));
|
||||
mocks.trash.empty.mockResolvedValue(1);
|
||||
await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 });
|
||||
expect(trashMock.empty).toHaveBeenCalledWith('user-id');
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
|
||||
expect(mocks.trash.empty).toHaveBeenCalledWith('user-id');
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAssetsDelete', () => {
|
||||
it('should queue the empty trash job', async () => {
|
||||
await expect(sut.onAssetsDelete()).resolves.toBeUndefined();
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueEmptyTrash', () => {
|
||||
it('should queue asset delete jobs', async () => {
|
||||
trashMock.getDeletedIds.mockReturnValue(makeAssetIdStream(1));
|
||||
mocks.trash.getDeletedIds.mockReturnValue(makeAssetIdStream(1));
|
||||
await expect(sut.handleQueueEmptyTrash()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.ASSET_DELETION,
|
||||
data: { id: 'asset-1', deleteOnDisk: true },
|
||||
|
|
|
@ -1,31 +1,28 @@
|
|||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import { mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { UserStatus } from 'src/enum';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { JobName } from 'src/interfaces/job.interface';
|
||||
import { UserAdminService } from 'src/services/user-admin.service';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked, describe } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { describe } from 'vitest';
|
||||
|
||||
describe(UserAdminService.name, () => {
|
||||
let sut: UserAdminService;
|
||||
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, jobMock, userMock } = newTestService(UserAdminService));
|
||||
({ sut, mocks } = newTestService(UserAdminService));
|
||||
|
||||
userMock.get.mockImplementation((userId) =>
|
||||
mocks.user.get.mockImplementation((userId) =>
|
||||
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? undefined),
|
||||
);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should not create a user if there is no local admin account', async () => {
|
||||
userMock.getAdmin.mockResolvedValueOnce(void 0);
|
||||
mocks.user.getAdmin.mockResolvedValueOnce(void 0);
|
||||
|
||||
await expect(
|
||||
sut.create({
|
||||
|
@ -37,8 +34,8 @@ describe(UserAdminService.name, () => {
|
|||
});
|
||||
|
||||
it('should create user', async () => {
|
||||
userMock.getAdmin.mockResolvedValue(userStub.admin);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
mocks.user.getAdmin.mockResolvedValue(userStub.admin);
|
||||
mocks.user.create.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(
|
||||
sut.create({
|
||||
|
@ -49,8 +46,8 @@ describe(UserAdminService.name, () => {
|
|||
}),
|
||||
).resolves.toEqual(mapUserAdmin(userStub.user1));
|
||||
|
||||
expect(userMock.getAdmin).toBeCalled();
|
||||
expect(userMock.create).toBeCalledWith({
|
||||
expect(mocks.user.getAdmin).toBeCalled();
|
||||
expect(mocks.user.create).toBeCalledWith({
|
||||
email: userStub.user1.email,
|
||||
name: userStub.user1.name,
|
||||
storageLabel: 'label',
|
||||
|
@ -66,20 +63,20 @@ describe(UserAdminService.name, () => {
|
|||
email: 'immich@test.com',
|
||||
storageLabel: 'storage_label',
|
||||
};
|
||||
userMock.getByEmail.mockResolvedValue(void 0);
|
||||
userMock.getByStorageLabel.mockResolvedValue(void 0);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
mocks.user.getByStorageLabel.mockResolvedValue(void 0);
|
||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await sut.update(authStub.user1, userStub.user1.id, update);
|
||||
|
||||
expect(userMock.getByEmail).toHaveBeenCalledWith(update.email);
|
||||
expect(userMock.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel);
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledWith(update.email);
|
||||
expect(mocks.user.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel);
|
||||
});
|
||||
|
||||
it('should not set an empty string for storage label', async () => {
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||
await sut.update(authStub.admin, userStub.user1.id, { storageLabel: '' });
|
||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(userStub.user1.id, {
|
||||
storageLabel: null,
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
|
@ -88,27 +85,27 @@ describe(UserAdminService.name, () => {
|
|||
it('should not change an email to one already in use', async () => {
|
||||
const dto = { id: userStub.user1.id, email: 'updated@test.com' };
|
||||
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userMock.getByEmail.mockResolvedValue(userStub.admin);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.getByEmail.mockResolvedValue(userStub.admin);
|
||||
|
||||
await expect(sut.update(authStub.admin, userStub.user1.id, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not let the admin change the storage label to one already in use', async () => {
|
||||
const dto = { id: userStub.user1.id, storageLabel: 'admin' };
|
||||
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userMock.getByStorageLabel.mockResolvedValue(userStub.admin);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.getByStorageLabel.mockResolvedValue(userStub.admin);
|
||||
|
||||
await expect(sut.update(authStub.admin, userStub.user1.id, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update user information should throw error if user not found', async () => {
|
||||
userMock.get.mockResolvedValueOnce(void 0);
|
||||
mocks.user.get.mockResolvedValueOnce(void 0);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, userStub.user1.id, { shouldChangePassword: true }),
|
||||
|
@ -118,10 +115,10 @@ describe(UserAdminService.name, () => {
|
|||
|
||||
describe('delete', () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
userMock.get.mockResolvedValue(void 0);
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
|
||||
await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toThrowError(BadRequestException);
|
||||
expect(userMock.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.user.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cannot delete admin user', async () => {
|
||||
|
@ -131,33 +128,33 @@ describe(UserAdminService.name, () => {
|
|||
it('should require the auth user be an admin', async () => {
|
||||
await expect(sut.delete(authStub.user1, authStub.admin.user.id, {})).rejects.toBeInstanceOf(ForbiddenException);
|
||||
|
||||
expect(userMock.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.user.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete user', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(sut.delete(authStub.admin, userStub.user1.id, {})).resolves.toEqual(mapUserAdmin(userStub.user1));
|
||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(userStub.user1.id, {
|
||||
status: UserStatus.DELETED,
|
||||
deletedAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('should force delete user', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(sut.delete(authStub.admin, userStub.user1.id, { force: true })).resolves.toEqual(
|
||||
mapUserAdmin(userStub.user1),
|
||||
);
|
||||
|
||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(userStub.user1.id, {
|
||||
status: UserStatus.REMOVING,
|
||||
deletedAt: expect.any(Date),
|
||||
});
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.USER_DELETION,
|
||||
data: { id: userStub.user1.id, force: true },
|
||||
});
|
||||
|
@ -166,16 +163,16 @@ describe(UserAdminService.name, () => {
|
|||
|
||||
describe('restore', () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
userMock.get.mockResolvedValue(void 0);
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
await expect(sut.restore(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException);
|
||||
expect(userMock.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should restore an user', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userMock.restore.mockResolvedValue(userStub.user1);
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
mocks.user.restore.mockResolvedValue(userStub.user1);
|
||||
await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUserAdmin(userStub.user1));
|
||||
expect(userMock.restore).toHaveBeenCalledWith(userStub.user1.id);
|
||||
expect(mocks.user.restore).toHaveBeenCalledWith(userStub.user1.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { CacheControl, UserMetadataKey } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { JobName } from 'src/interfaces/job.interface';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
import { ISystemMetadataRepository } from 'src/types';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const makeDeletedAt = (daysAgo: number) => {
|
||||
const deletedAt = new Date();
|
||||
|
@ -22,68 +17,63 @@ const makeDeletedAt = (daysAgo: number) => {
|
|||
|
||||
describe(UserService.name, () => {
|
||||
let sut: UserService;
|
||||
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, albumMock, jobMock, storageMock, systemMock, userMock } = newTestService(UserService));
|
||||
({ sut, mocks } = newTestService(UserService));
|
||||
|
||||
userMock.get.mockImplementation((userId) =>
|
||||
mocks.user.get.mockImplementation((userId) =>
|
||||
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? undefined),
|
||||
);
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('admin should get all users', async () => {
|
||||
userMock.getList.mockResolvedValue([userStub.admin]);
|
||||
mocks.user.getList.mockResolvedValue([userStub.admin]);
|
||||
await expect(sut.search(authStub.admin)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: authStub.admin.user.id,
|
||||
email: authStub.admin.user.email,
|
||||
}),
|
||||
]);
|
||||
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: false });
|
||||
expect(mocks.user.getList).toHaveBeenCalledWith({ withDeleted: false });
|
||||
});
|
||||
|
||||
it('non-admin should get all users when publicUsers enabled', async () => {
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
await expect(sut.search(authStub.user1)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: authStub.user1.user.id,
|
||||
email: authStub.user1.user.email,
|
||||
}),
|
||||
]);
|
||||
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: false });
|
||||
expect(mocks.user.getList).toHaveBeenCalledWith({ withDeleted: false });
|
||||
});
|
||||
|
||||
it('non-admin user should only receive itself when publicUsers is disabled', async () => {
|
||||
userMock.getList.mockResolvedValue([userStub.user1]);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.publicUsersDisabled);
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.publicUsersDisabled);
|
||||
await expect(sut.search(authStub.user1)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: authStub.user1.user.id,
|
||||
email: authStub.user1.user.email,
|
||||
}),
|
||||
]);
|
||||
expect(userMock.getList).not.toHaveBeenCalledWith({ withDeleted: false });
|
||||
expect(mocks.user.getList).not.toHaveBeenCalledWith({ withDeleted: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should get a user by id', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
await sut.get(authStub.admin.user.id);
|
||||
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
|
||||
});
|
||||
|
||||
it('should throw an error if a user is not found', async () => {
|
||||
userMock.get.mockResolvedValue(void 0);
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
await expect(sut.get(authStub.admin.user.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -100,78 +90,78 @@ describe(UserService.name, () => {
|
|||
describe('createProfileImage', () => {
|
||||
it('should throw an error if the user does not exist', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
userMock.get.mockResolvedValue(void 0);
|
||||
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
mocks.user.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
|
||||
await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw an error if the user profile could not be updated with the new image', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||
userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
|
||||
mocks.user.get.mockResolvedValue(userStub.profilePath);
|
||||
mocks.user.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
|
||||
|
||||
await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(InternalServerErrorException);
|
||||
});
|
||||
|
||||
it('should delete the previous profile image', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||
mocks.user.get.mockResolvedValue(userStub.profilePath);
|
||||
const files = [userStub.profilePath.profileImagePath];
|
||||
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
mocks.user.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
|
||||
await sut.createProfileImage(authStub.admin, file);
|
||||
expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
|
||||
expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
|
||||
});
|
||||
|
||||
it('should not delete the profile image if it has not been set', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.user.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
|
||||
await sut.createProfileImage(authStub.admin, file);
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteProfileImage', () => {
|
||||
it('should send an http error has no profile image', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
|
||||
await expect(sut.deleteProfileImage(authStub.admin)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete the profile image if user has one', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||
mocks.user.get.mockResolvedValue(userStub.profilePath);
|
||||
const files = [userStub.profilePath.profileImagePath];
|
||||
|
||||
await sut.deleteProfileImage(authStub.admin);
|
||||
expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
|
||||
expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserProfileImage', () => {
|
||||
it('should throw an error if the user does not exist', async () => {
|
||||
userMock.get.mockResolvedValue(void 0);
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
|
||||
await expect(sut.getProfileImage(userStub.admin.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.admin.id, {});
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(userStub.admin.id, {});
|
||||
});
|
||||
|
||||
it('should throw an error if the user does not have a picture', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
|
||||
await expect(sut.getProfileImage(userStub.admin.id)).rejects.toBeInstanceOf(NotFoundException);
|
||||
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.admin.id, {});
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(userStub.admin.id, {});
|
||||
});
|
||||
|
||||
it('should return the profile picture', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||
mocks.user.get.mockResolvedValue(userStub.profilePath);
|
||||
|
||||
await expect(sut.getProfileImage(userStub.profilePath.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
|
@ -181,13 +171,13 @@ describe(UserService.name, () => {
|
|||
}),
|
||||
);
|
||||
|
||||
expect(userMock.get).toHaveBeenCalledWith(userStub.profilePath.id, {});
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(userStub.profilePath.id, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueUserDelete', () => {
|
||||
it('should skip users not ready for deletion', async () => {
|
||||
userMock.getDeletedUsers.mockResolvedValue([
|
||||
mocks.user.getDeletedUsers.mockResolvedValue([
|
||||
{},
|
||||
{ deletedAt: undefined },
|
||||
{ deletedAt: null },
|
||||
|
@ -196,14 +186,14 @@ describe(UserService.name, () => {
|
|||
|
||||
await sut.handleUserDeleteCheck();
|
||||
|
||||
expect(userMock.getDeletedUsers).toHaveBeenCalled();
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([]);
|
||||
expect(mocks.user.getDeletedUsers).toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should skip users not ready for deletion - deleteDelay30', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.deleteDelay30);
|
||||
userMock.getDeletedUsers.mockResolvedValue([
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.deleteDelay30);
|
||||
mocks.user.getDeletedUsers.mockResolvedValue([
|
||||
{},
|
||||
{ deletedAt: undefined },
|
||||
{ deletedAt: null },
|
||||
|
@ -212,120 +202,120 @@ describe(UserService.name, () => {
|
|||
|
||||
await sut.handleUserDeleteCheck();
|
||||
|
||||
expect(userMock.getDeletedUsers).toHaveBeenCalled();
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([]);
|
||||
expect(mocks.user.getDeletedUsers).toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should queue user ready for deletion', async () => {
|
||||
const user = { id: 'deleted-user', deletedAt: makeDeletedAt(10) };
|
||||
userMock.getDeletedUsers.mockResolvedValue([user] as UserEntity[]);
|
||||
mocks.user.getDeletedUsers.mockResolvedValue([user] as UserEntity[]);
|
||||
|
||||
await sut.handleUserDeleteCheck();
|
||||
|
||||
expect(userMock.getDeletedUsers).toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.USER_DELETION, data: { id: user.id } }]);
|
||||
expect(mocks.user.getDeletedUsers).toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.USER_DELETION, data: { id: user.id } }]);
|
||||
});
|
||||
|
||||
it('should queue user ready for deletion - deleteDelay30', async () => {
|
||||
const user = { id: 'deleted-user', deletedAt: makeDeletedAt(31) };
|
||||
userMock.getDeletedUsers.mockResolvedValue([user] as UserEntity[]);
|
||||
mocks.user.getDeletedUsers.mockResolvedValue([user] as UserEntity[]);
|
||||
|
||||
await sut.handleUserDeleteCheck();
|
||||
|
||||
expect(userMock.getDeletedUsers).toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.USER_DELETION, data: { id: user.id } }]);
|
||||
expect(mocks.user.getDeletedUsers).toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.USER_DELETION, data: { id: user.id } }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUserDelete', () => {
|
||||
it('should skip users not ready for deletion', async () => {
|
||||
const user = { id: 'user-1', deletedAt: makeDeletedAt(5) } as UserEntity;
|
||||
userMock.get.mockResolvedValue(user);
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
|
||||
await sut.handleUserDelete({ id: user.id });
|
||||
|
||||
expect(storageMock.unlinkDir).not.toHaveBeenCalled();
|
||||
expect(userMock.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.unlinkDir).not.toHaveBeenCalled();
|
||||
expect(mocks.user.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete the user and associated assets', async () => {
|
||||
const user = { id: 'deleted-user', deletedAt: makeDeletedAt(10) } as UserEntity;
|
||||
userMock.get.mockResolvedValue(user);
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
|
||||
await sut.handleUserDelete({ id: user.id });
|
||||
|
||||
const options = { force: true, recursive: true };
|
||||
|
||||
expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/library/deleted-user', options);
|
||||
expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/upload/deleted-user', options);
|
||||
expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/profile/deleted-user', options);
|
||||
expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/thumbs/deleted-user', options);
|
||||
expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/encoded-video/deleted-user', options);
|
||||
expect(albumMock.deleteAll).toHaveBeenCalledWith(user.id);
|
||||
expect(userMock.delete).toHaveBeenCalledWith(user, true);
|
||||
expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/library/deleted-user', options);
|
||||
expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/upload/deleted-user', options);
|
||||
expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/profile/deleted-user', options);
|
||||
expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/thumbs/deleted-user', options);
|
||||
expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/encoded-video/deleted-user', options);
|
||||
expect(mocks.album.deleteAll).toHaveBeenCalledWith(user.id);
|
||||
expect(mocks.user.delete).toHaveBeenCalledWith(user, true);
|
||||
});
|
||||
|
||||
it('should delete the library path for a storage label', async () => {
|
||||
const user = { id: 'deleted-user', deletedAt: makeDeletedAt(10), storageLabel: 'admin' } as UserEntity;
|
||||
userMock.get.mockResolvedValue(user);
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
|
||||
await sut.handleUserDelete({ id: user.id });
|
||||
|
||||
const options = { force: true, recursive: true };
|
||||
|
||||
expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/library/admin', options);
|
||||
expect(mocks.storage.unlinkDir).toHaveBeenCalledWith('upload/library/admin', options);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLicense', () => {
|
||||
it('should save client license if valid', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
mocks.user.upsertMetadata.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'IMCL-license-key', activationKey: 'activation-key' };
|
||||
await sut.setLicense(authStub.user1, license);
|
||||
|
||||
expect(userMock.upsertMetadata).toHaveBeenCalledWith(authStub.user1.user.id, {
|
||||
expect(mocks.user.upsertMetadata).toHaveBeenCalledWith(authStub.user1.user.id, {
|
||||
key: UserMetadataKey.LICENSE,
|
||||
value: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('should save server license as client if valid', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
mocks.user.upsertMetadata.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'IMSV-license-key', activationKey: 'activation-key' };
|
||||
await sut.setLicense(authStub.user1, license);
|
||||
|
||||
expect(userMock.upsertMetadata).toHaveBeenCalledWith(authStub.user1.user.id, {
|
||||
expect(mocks.user.upsertMetadata).toHaveBeenCalledWith(authStub.user1.user.id, {
|
||||
key: UserMetadataKey.LICENSE,
|
||||
value: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not save license if invalid', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
mocks.user.upsertMetadata.mockResolvedValue();
|
||||
|
||||
const license = { licenseKey: 'license-key', activationKey: 'activation-key' };
|
||||
const call = sut.setLicense(authStub.admin, license);
|
||||
await expect(call).rejects.toThrowError('Invalid license key');
|
||||
expect(userMock.upsertMetadata).not.toHaveBeenCalled();
|
||||
expect(mocks.user.upsertMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteLicense', () => {
|
||||
it('should delete license', async () => {
|
||||
userMock.upsertMetadata.mockResolvedValue();
|
||||
mocks.user.upsertMetadata.mockResolvedValue();
|
||||
|
||||
await sut.deleteLicense(authStub.admin);
|
||||
expect(userMock.upsertMetadata).not.toHaveBeenCalled();
|
||||
expect(mocks.user.upsertMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUserSyncUsage', () => {
|
||||
it('should sync usage', async () => {
|
||||
await sut.handleUserSyncUsage();
|
||||
expect(userMock.syncUsage).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.user.syncUsage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,19 +2,10 @@ import { DateTime } from 'luxon';
|
|||
import { SemVer } from 'semver';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import {
|
||||
IConfigRepository,
|
||||
ILoggingRepository,
|
||||
IServerInfoRepository,
|
||||
ISystemMetadataRepository,
|
||||
IVersionHistoryRepository,
|
||||
} from 'src/types';
|
||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const mockRelease = (version: string) => ({
|
||||
id: 1,
|
||||
|
@ -28,18 +19,10 @@ const mockRelease = (version: string) => ({
|
|||
|
||||
describe(VersionService.name, () => {
|
||||
let sut: VersionService;
|
||||
|
||||
let configMock: Mocked<IConfigRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let loggerMock: Mocked<ILoggingRepository>;
|
||||
let serverInfoMock: Mocked<IServerInfoRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let versionHistoryMock: Mocked<IVersionHistoryRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, configMock, eventMock, jobMock, loggerMock, serverInfoMock, systemMock, versionHistoryMock } =
|
||||
newTestService(VersionService));
|
||||
({ sut, mocks } = newTestService(VersionService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -49,17 +32,17 @@ describe(VersionService.name, () => {
|
|||
describe('onBootstrap', () => {
|
||||
it('should record a new version', async () => {
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
expect(versionHistoryMock.create).toHaveBeenCalledWith({ version: expect.any(String) });
|
||||
expect(mocks.versionHistory.create).toHaveBeenCalledWith({ version: expect.any(String) });
|
||||
});
|
||||
|
||||
it('should skip a duplicate version', async () => {
|
||||
versionHistoryMock.getLatest.mockResolvedValue({
|
||||
mocks.versionHistory.getLatest.mockResolvedValue({
|
||||
id: 'version-1',
|
||||
createdAt: new Date(),
|
||||
version: serverVersion.toString(),
|
||||
});
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
expect(versionHistoryMock.create).not.toHaveBeenCalled();
|
||||
expect(mocks.versionHistory.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -76,7 +59,7 @@ describe(VersionService.name, () => {
|
|||
describe('getVersionHistory', () => {
|
||||
it('should respond the server version history', async () => {
|
||||
const upgrade = { id: 'upgrade-1', createdAt: new Date(), version: '1.0.0' };
|
||||
versionHistoryMock.getAll.mockResolvedValue([upgrade]);
|
||||
mocks.versionHistory.getAll.mockResolvedValue([upgrade]);
|
||||
await expect(sut.getVersionHistory()).resolves.toEqual([upgrade]);
|
||||
});
|
||||
});
|
||||
|
@ -84,22 +67,22 @@ describe(VersionService.name, () => {
|
|||
describe('handQueueVersionCheck', () => {
|
||||
it('should queue a version check job', async () => {
|
||||
await expect(sut.handleQueueVersionCheck()).resolves.toBeUndefined();
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.VERSION_CHECK, data: {} });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VERSION_CHECK, data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handVersionCheck', () => {
|
||||
beforeEach(() => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.PRODUCTION }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.PRODUCTION }));
|
||||
});
|
||||
|
||||
it('should not run in dev mode', async () => {
|
||||
configMock.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.DEVELOPMENT }));
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.DEVELOPMENT }));
|
||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED);
|
||||
});
|
||||
|
||||
it('should not run if the last check was < 60 minutes ago', async () => {
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
checkedAt: DateTime.utc().minus({ minutes: 5 }).toISO(),
|
||||
releaseVersion: '1.0.0',
|
||||
});
|
||||
|
@ -107,53 +90,53 @@ describe(VersionService.name, () => {
|
|||
});
|
||||
|
||||
it('should not run if version check is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue({ newVersionCheck: { enabled: false } });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ newVersionCheck: { enabled: false } });
|
||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED);
|
||||
});
|
||||
|
||||
it('should run if it has been > 60 minutes', async () => {
|
||||
serverInfoMock.getGitHubRelease.mockResolvedValue(mockRelease('v100.0.0'));
|
||||
systemMock.get.mockResolvedValue({
|
||||
mocks.serverInfo.getGitHubRelease.mockResolvedValue(mockRelease('v100.0.0'));
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
checkedAt: DateTime.utc().minus({ minutes: 65 }).toISO(),
|
||||
releaseVersion: '1.0.0',
|
||||
});
|
||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
expect(systemMock.set).toHaveBeenCalled();
|
||||
expect(loggerMock.log).toHaveBeenCalled();
|
||||
expect(eventMock.clientBroadcast).toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalled();
|
||||
expect(mocks.logger.log).toHaveBeenCalled();
|
||||
expect(mocks.event.clientBroadcast).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not notify if the version is equal', async () => {
|
||||
serverInfoMock.getGitHubRelease.mockResolvedValue(mockRelease(serverVersion.toString()));
|
||||
mocks.serverInfo.getGitHubRelease.mockResolvedValue(mockRelease(serverVersion.toString()));
|
||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.VERSION_CHECK_STATE, {
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.VERSION_CHECK_STATE, {
|
||||
checkedAt: expect.any(String),
|
||||
releaseVersion: serverVersion.toString(),
|
||||
});
|
||||
expect(eventMock.clientBroadcast).not.toHaveBeenCalled();
|
||||
expect(mocks.event.clientBroadcast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a github error', async () => {
|
||||
serverInfoMock.getGitHubRelease.mockRejectedValue(new Error('GitHub is down'));
|
||||
mocks.serverInfo.getGitHubRelease.mockRejectedValue(new Error('GitHub is down'));
|
||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.FAILED);
|
||||
expect(systemMock.set).not.toHaveBeenCalled();
|
||||
expect(eventMock.clientBroadcast).not.toHaveBeenCalled();
|
||||
expect(loggerMock.warn).toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
||||
expect(mocks.event.clientBroadcast).not.toHaveBeenCalled();
|
||||
expect(mocks.logger.warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onWebsocketConnectionEvent', () => {
|
||||
it('should send on_server_version client event', async () => {
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(eventMock.clientSend).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should also send a new release notification', async () => {
|
||||
systemMock.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' });
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(eventMock.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.event.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { ViewService } from 'src/services/view.service';
|
||||
import { IViewRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
|
||||
import { Mocked } from 'vitest';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(ViewService.name, () => {
|
||||
let sut: ViewService;
|
||||
let viewMock: Mocked<IViewRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, viewMock } = newTestService(ViewService));
|
||||
({ sut, mocks } = newTestService(ViewService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -22,12 +19,12 @@ describe(ViewService.name, () => {
|
|||
describe('getUniqueOriginalPaths', () => {
|
||||
it('should return unique original paths', async () => {
|
||||
const mockPaths = ['path1', 'path2', 'path3'];
|
||||
viewMock.getUniqueOriginalPaths.mockResolvedValue(mockPaths);
|
||||
mocks.view.getUniqueOriginalPaths.mockResolvedValue(mockPaths);
|
||||
|
||||
const result = await sut.getUniqueOriginalPaths(authStub.admin);
|
||||
|
||||
expect(result).toEqual(mockPaths);
|
||||
expect(viewMock.getUniqueOriginalPaths).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(mocks.view.getUniqueOriginalPaths).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -42,11 +39,11 @@ describe(ViewService.name, () => {
|
|||
|
||||
const mockAssetReponseDto = mockAssets.map((a) => mapAsset(a, { auth: authStub.admin }));
|
||||
|
||||
viewMock.getAssetsByOriginalPath.mockResolvedValue(mockAssets as any);
|
||||
mocks.view.getAssetsByOriginalPath.mockResolvedValue(mockAssets as any);
|
||||
|
||||
const result = await sut.getAssetsByOriginalPath(authStub.admin, path);
|
||||
expect(result).toEqual(mockAssetReponseDto);
|
||||
await expect(viewMock.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets);
|
||||
await expect(mocks.view.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,27 +1,9 @@
|
|||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { ExifOrientation, ImageFormat, Permission, TranscodeTarget, VideoCodec } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CronRepository } from 'src/repositories/cron.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MapRepository } from 'src/repositories/map.repository';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { MetricGroupRepository, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { TrashRepository } from 'src/repositories/trash.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
|
||||
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
|
||||
|
||||
|
@ -34,41 +16,10 @@ export type AuthApiKey = {
|
|||
|
||||
export type RepositoryInterface<T extends object> = Pick<T, keyof T>;
|
||||
|
||||
export type IActivityRepository = RepositoryInterface<ActivityRepository>;
|
||||
export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface<AccessRepository[K]> };
|
||||
export type IAlbumUserRepository = RepositoryInterface<AlbumUserRepository>;
|
||||
export type IApiKeyRepository = RepositoryInterface<ApiKeyRepository>;
|
||||
export type IAuditRepository = RepositoryInterface<AuditRepository>;
|
||||
export type IConfigRepository = RepositoryInterface<ConfigRepository>;
|
||||
export type ICronRepository = RepositoryInterface<CronRepository>;
|
||||
export type ILoggingRepository = Pick<
|
||||
LoggingRepository,
|
||||
| 'verbose'
|
||||
| 'log'
|
||||
| 'debug'
|
||||
| 'warn'
|
||||
| 'error'
|
||||
| 'fatal'
|
||||
| 'isLevelEnabled'
|
||||
| 'setLogLevel'
|
||||
| 'setContext'
|
||||
| 'setAppName'
|
||||
>;
|
||||
export type IMapRepository = RepositoryInterface<MapRepository>;
|
||||
export type IMediaRepository = RepositoryInterface<MediaRepository>;
|
||||
export type IMemoryRepository = RepositoryInterface<MemoryRepository>;
|
||||
export type IMetadataRepository = RepositoryInterface<MetadataRepository>;
|
||||
export type IMetricGroupRepository = RepositoryInterface<MetricGroupRepository>;
|
||||
export type INotificationRepository = RepositoryInterface<NotificationRepository>;
|
||||
export type IOAuthRepository = RepositoryInterface<OAuthRepository>;
|
||||
export type IProcessRepository = RepositoryInterface<ProcessRepository>;
|
||||
export type ISessionRepository = RepositoryInterface<SessionRepository>;
|
||||
export type IServerInfoRepository = RepositoryInterface<ServerInfoRepository>;
|
||||
export type ISystemMetadataRepository = RepositoryInterface<SystemMetadataRepository>;
|
||||
export type ITelemetryRepository = RepositoryInterface<TelemetryRepository>;
|
||||
export type ITrashRepository = RepositoryInterface<TrashRepository>;
|
||||
export type IViewRepository = RepositoryInterface<ViewRepository>;
|
||||
export type IVersionHistoryRepository = RepositoryInterface<VersionHistoryRepository>;
|
||||
type IActivityRepository = RepositoryInterface<ActivityRepository>;
|
||||
type IApiKeyRepository = RepositoryInterface<ApiKeyRepository>;
|
||||
type IMemoryRepository = RepositoryInterface<MemoryRepository>;
|
||||
type ISessionRepository = RepositoryInterface<SessionRepository>;
|
||||
|
||||
export type ActivityItem =
|
||||
| Awaited<ReturnType<IActivityRepository['create']>>
|
||||
|
|
|
@ -7,15 +7,18 @@ import { SystemConfig, defaults } from 'src/config';
|
|||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||
import { DeepPartial, IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { DeepPartial } from 'src/types';
|
||||
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
|
||||
|
||||
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
||||
|
||||
type RepoDeps = {
|
||||
configRepo: IConfigRepository;
|
||||
metadataRepo: ISystemMetadataRepository;
|
||||
logger: ILoggingRepository;
|
||||
configRepo: ConfigRepository;
|
||||
metadataRepo: SystemMetadataRepository;
|
||||
logger: LoggingRepository;
|
||||
};
|
||||
|
||||
const asyncLock = new AsyncLock();
|
||||
|
|
|
@ -5,7 +5,7 @@ import { basename, extname, isAbsolute } from 'node:path';
|
|||
import { promisify } from 'node:util';
|
||||
import { CacheControl } from 'src/enum';
|
||||
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
||||
import { ILoggingRepository } from 'src/types';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { isConnectionAborted } from 'src/utils/misc';
|
||||
|
||||
export function getFileNameWithoutExtension(path: string): string {
|
||||
|
@ -37,7 +37,7 @@ export const sendFile = async (
|
|||
res: Response,
|
||||
next: NextFunction,
|
||||
handler: () => Promise<ImmichFileResponse>,
|
||||
logger: ILoggingRepository,
|
||||
logger: LoggingRepository,
|
||||
): Promise<void> => {
|
||||
const _sendFile = (path: string, options: SendFileOptions) =>
|
||||
promisify<string, SendFileOptions>(res.sendFile).bind(res)(path, options);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { HttpException } from '@nestjs/common';
|
||||
import { ILoggingRepository } from 'src/types';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { TypeORMError } from 'typeorm';
|
||||
|
||||
export const logGlobalError = (logger: ILoggingRepository, error: Error) => {
|
||||
export const logGlobalError = (logger: LoggingRepository, error: Error) => {
|
||||
if (error instanceof HttpException) {
|
||||
const status = error.getStatus();
|
||||
const response = error.getResponse();
|
||||
|
|
|
@ -13,7 +13,7 @@ import path from 'node:path';
|
|||
import { SystemConfig } from 'src/config';
|
||||
import { CLIP_MODEL_INFO, serverVersion } from 'src/constants';
|
||||
import { ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
|
||||
import { ILoggingRepository } from 'src/types';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export class ImmichStartupError extends Error {}
|
||||
export const isStartUpError = (error: unknown): error is ImmichStartupError => error instanceof ImmichStartupError;
|
||||
|
@ -96,7 +96,7 @@ export const isFaceImportEnabled = (metadata: SystemConfig['metadata']) => metad
|
|||
|
||||
export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
|
||||
|
||||
export const handlePromiseError = <T>(promise: Promise<T>, logger: ILoggingRepository): void => {
|
||||
export const handlePromiseError = <T>(promise: Promise<T>, logger: LoggingRepository): void => {
|
||||
promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack));
|
||||
};
|
||||
|
||||
|
|
|
@ -3,15 +3,11 @@ import { writeFile } from 'node:fs/promises';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MetadataService } from 'src/services/metadata.service';
|
||||
import { ILoggingRepository } from 'src/types';
|
||||
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newRandomImage, newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newRandomImage, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const metadataRepository = new MetadataRepository(
|
||||
newLoggingRepositoryMock() as ILoggingRepository as LoggingRepository,
|
||||
|
@ -38,14 +34,12 @@ type TimeZoneTest = {
|
|||
|
||||
describe(MetadataService.name, () => {
|
||||
let sut: MetadataService;
|
||||
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, assetMock, storageMock } = newTestService(MetadataService, { metadataRepository }));
|
||||
({ sut, mocks } = newTestService(MetadataService, { metadataRepository }));
|
||||
|
||||
storageMock.stat.mockResolvedValue({ size: 123_456 } as Stats);
|
||||
mocks.storage.stat.mockResolvedValue({ size: 123_456 } as Stats);
|
||||
|
||||
delete process.env.TZ;
|
||||
});
|
||||
|
@ -120,18 +114,18 @@ describe(MetadataService.name, () => {
|
|||
process.env.TZ = serverTimeZone ?? undefined;
|
||||
|
||||
const { filePath } = await createTestFile(exifData);
|
||||
assetMock.getByIds.mockResolvedValue([{ id: 'asset-1', originalPath: filePath } as AssetEntity]);
|
||||
mocks.asset.getByIds.mockResolvedValue([{ id: 'asset-1', originalPath: filePath } as AssetEntity]);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: 'asset-1' });
|
||||
|
||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dateTimeOriginal: new Date(expected.dateTimeOriginal),
|
||||
timeZone: expected.timeZone,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith(
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
localDateTime: new Date(expected.localDateTime),
|
||||
}),
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { IAccessRepository } from 'src/types';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export type IAccessRepositoryMock = { [K in keyof IAccessRepository]: Mocked<IAccessRepository[K]> };
|
||||
type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface<AccessRepository[K]> };
|
||||
|
||||
export type IAccessRepositoryMock = {
|
||||
[K in keyof IAccessRepository]: Mocked<IAccessRepository[K]>;
|
||||
};
|
||||
|
||||
export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
return {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IActivityRepository } from 'src/types';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newActivityRepositoryMock = (): Mocked<IActivityRepository> => {
|
||||
export const newActivityRepositoryMock = (): Mocked<RepositoryInterface<ActivityRepository>> => {
|
||||
return {
|
||||
search: vitest.fn(),
|
||||
create: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IAlbumUserRepository } from 'src/types';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
export const newAlbumUserRepositoryMock = (): Mocked<IAlbumUserRepository> => {
|
||||
export const newAlbumUserRepositoryMock = (): Mocked<RepositoryInterface<AlbumUserRepository>> => {
|
||||
return {
|
||||
create: vitest.fn(),
|
||||
delete: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IApiKeyRepository } from 'src/types';
|
||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newKeyRepositoryMock = (): Mocked<IApiKeyRepository> => {
|
||||
export const newKeyRepositoryMock = (): Mocked<RepositoryInterface<ApiKeyRepository>> => {
|
||||
return {
|
||||
create: vitest.fn(),
|
||||
update: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IAuditRepository } from 'src/types';
|
||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newAuditRepositoryMock = (): Mocked<IAuditRepository> => {
|
||||
export const newAuditRepositoryMock = (): Mocked<RepositoryInterface<AuditRepository>> => {
|
||||
return {
|
||||
getAfter: vitest.fn(),
|
||||
removeBefore: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ImmichEnvironment, ImmichWorker } from 'src/enum';
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { EnvData } from 'src/repositories/config.repository';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { ConfigRepository, EnvData } from 'src/repositories/config.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
const envData: EnvData = {
|
||||
|
@ -97,7 +97,7 @@ const envData: EnvData = {
|
|||
};
|
||||
|
||||
export const mockEnvData = (config: Partial<EnvData>) => ({ ...envData, ...config });
|
||||
export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
|
||||
export const newConfigRepositoryMock = (): Mocked<RepositoryInterface<ConfigRepository>> => {
|
||||
return {
|
||||
getEnv: vitest.fn().mockReturnValue(mockEnvData({})),
|
||||
getWorker: vitest.fn().mockReturnValue(ImmichWorker.API),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { ICronRepository } from 'src/types';
|
||||
import { CronRepository } from 'src/repositories/cron.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newCronRepositoryMock = (): Mocked<ICronRepository> => {
|
||||
export const newCronRepositoryMock = (): Mocked<RepositoryInterface<CronRepository>> => {
|
||||
return {
|
||||
create: vitest.fn(),
|
||||
update: vitest.fn(),
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
import { ILoggingRepository } from 'src/types';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export type ILoggingRepository = Pick<
|
||||
LoggingRepository,
|
||||
| 'verbose'
|
||||
| 'log'
|
||||
| 'debug'
|
||||
| 'warn'
|
||||
| 'error'
|
||||
| 'fatal'
|
||||
| 'isLevelEnabled'
|
||||
| 'setLogLevel'
|
||||
| 'setContext'
|
||||
| 'setAppName'
|
||||
>;
|
||||
|
||||
export const newLoggingRepositoryMock = (): Mocked<ILoggingRepository> => {
|
||||
return {
|
||||
setLogLevel: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IMapRepository } from 'src/types';
|
||||
import { MapRepository } from 'src/repositories/map.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
export const newMapRepositoryMock = (): Mocked<IMapRepository> => {
|
||||
export const newMapRepositoryMock = (): Mocked<RepositoryInterface<MapRepository>> => {
|
||||
return {
|
||||
init: vitest.fn(),
|
||||
reverseGeocode: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IMediaRepository } from 'src/types';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newMediaRepositoryMock = (): Mocked<IMediaRepository> => {
|
||||
export const newMediaRepositoryMock = (): Mocked<RepositoryInterface<MediaRepository>> => {
|
||||
return {
|
||||
generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IMemoryRepository } from 'src/types';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newMemoryRepositoryMock = (): Mocked<IMemoryRepository> => {
|
||||
export const newMemoryRepositoryMock = (): Mocked<RepositoryInterface<MemoryRepository>> => {
|
||||
return {
|
||||
search: vitest.fn().mockResolvedValue([]),
|
||||
get: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IMetadataRepository } from 'src/types';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newMetadataRepositoryMock = (): Mocked<IMetadataRepository> => {
|
||||
export const newMetadataRepositoryMock = (): Mocked<RepositoryInterface<MetadataRepository>> => {
|
||||
return {
|
||||
teardown: vitest.fn(),
|
||||
readTags: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { INotificationRepository } from 'src/types';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
export const newNotificationRepositoryMock = (): Mocked<INotificationRepository> => {
|
||||
export const newNotificationRepositoryMock = (): Mocked<RepositoryInterface<NotificationRepository>> => {
|
||||
return {
|
||||
renderEmail: vitest.fn(),
|
||||
sendEmail: vitest.fn().mockResolvedValue({ messageId: 'message-1' }),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IOAuthRepository } from 'src/types';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
export const newOAuthRepositoryMock = (): Mocked<IOAuthRepository> => {
|
||||
export const newOAuthRepositoryMock = (): Mocked<RepositoryInterface<OAuthRepository>> => {
|
||||
return {
|
||||
init: vitest.fn(),
|
||||
authorize: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IProcessRepository } from 'src/types';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newProcessRepositoryMock = (): Mocked<IProcessRepository> => {
|
||||
export const newProcessRepositoryMock = (): Mocked<RepositoryInterface<ProcessRepository>> => {
|
||||
return {
|
||||
spawn: vitest.fn(),
|
||||
};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IServerInfoRepository } from 'src/types';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newServerInfoRepositoryMock = (): Mocked<IServerInfoRepository> => {
|
||||
export const newServerInfoRepositoryMock = (): Mocked<RepositoryInterface<ServerInfoRepository>> => {
|
||||
return {
|
||||
getGitHubRelease: vitest.fn(),
|
||||
getBuildVersions: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { ISessionRepository } from 'src/types';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {
|
||||
export const newSessionRepositoryMock = (): Mocked<RepositoryInterface<SessionRepository>> => {
|
||||
return {
|
||||
search: vitest.fn(),
|
||||
create: vitest.fn() as any,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { ISystemMetadataRepository } from 'src/types';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { clearConfigCache } from 'src/utils/config';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newSystemMetadataRepositoryMock = (): Mocked<ISystemMetadataRepository> => {
|
||||
export const newSystemMetadataRepositoryMock = (): Mocked<RepositoryInterface<SystemMetadataRepository>> => {
|
||||
clearConfigCache();
|
||||
return {
|
||||
get: vitest.fn() as any,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ITelemetryRepository, RepositoryInterface } from 'src/types';
|
||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
const newMetricGroupMock = () => {
|
||||
|
@ -10,6 +11,8 @@ const newMetricGroupMock = () => {
|
|||
};
|
||||
};
|
||||
|
||||
type ITelemetryRepository = RepositoryInterface<TelemetryRepository>;
|
||||
|
||||
export type ITelemetryRepositoryMock = {
|
||||
[K in keyof ITelemetryRepository]: Mocked<RepositoryInterface<ITelemetryRepository[K]>>;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { ITrashRepository } from 'src/types';
|
||||
import { TrashRepository } from 'src/repositories/trash.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newTrashRepositoryMock = (): Mocked<ITrashRepository> => {
|
||||
export const newTrashRepositoryMock = (): Mocked<RepositoryInterface<TrashRepository>> => {
|
||||
return {
|
||||
empty: vitest.fn(),
|
||||
restore: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IVersionHistoryRepository } from 'src/types';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newVersionHistoryRepositoryMock = (): Mocked<IVersionHistoryRepository> => {
|
||||
export const newVersionHistoryRepositoryMock = (): Mocked<RepositoryInterface<VersionHistoryRepository>> => {
|
||||
return {
|
||||
getAll: vitest.fn().mockResolvedValue([]),
|
||||
getLatest: vitest.fn(),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { IViewRepository } from 'src/types';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newViewRepositoryMock = (): Mocked<IViewRepository> => {
|
||||
export const newViewRepositoryMock = (): Mocked<RepositoryInterface<ViewRepository>> => {
|
||||
return {
|
||||
getAssetsByOriginalPath: vitest.fn(),
|
||||
getUniqueOriginalPaths: vitest.fn(),
|
||||
|
|
|
@ -2,51 +2,49 @@ import { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|||
import { Writable } from 'node:stream';
|
||||
import { PNG } from 'pngjs';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CronRepository } from 'src/repositories/cron.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MapRepository } from 'src/repositories/map.repository';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { StackRepository } from 'src/repositories/stack.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TagRepository } from 'src/repositories/tag.repository';
|
||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { TrashRepository } from 'src/repositories/trash.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import {
|
||||
IAccessRepository,
|
||||
IActivityRepository,
|
||||
IAlbumUserRepository,
|
||||
IApiKeyRepository,
|
||||
IAuditRepository,
|
||||
ICronRepository,
|
||||
ILoggingRepository,
|
||||
IMapRepository,
|
||||
IMediaRepository,
|
||||
IMemoryRepository,
|
||||
IMetadataRepository,
|
||||
INotificationRepository,
|
||||
IOAuthRepository,
|
||||
IProcessRepository,
|
||||
IServerInfoRepository,
|
||||
ISessionRepository,
|
||||
ISystemMetadataRepository,
|
||||
ITrashRepository,
|
||||
IVersionHistoryRepository,
|
||||
IViewRepository,
|
||||
} from 'src/types';
|
||||
import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock';
|
||||
import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
|
@ -60,7 +58,7 @@ import { newDatabaseRepositoryMock } from 'test/repositories/database.repository
|
|||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
||||
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
|
||||
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
|
||||
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
||||
|
@ -80,7 +78,7 @@ import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock'
|
|||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock';
|
||||
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||
import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||
import { newTrashRepositoryMock } from 'test/repositories/trash.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock';
|
||||
|
@ -97,6 +95,50 @@ type Constructor<Type, Args extends Array<any>> = {
|
|||
new (...deps: Args): Type;
|
||||
};
|
||||
|
||||
type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface<AccessRepository[K]> };
|
||||
|
||||
export type ServiceMocks = {
|
||||
access: IAccessRepositoryMock;
|
||||
activity: Mocked<RepositoryInterface<ActivityRepository>>;
|
||||
album: Mocked<IAlbumRepository>;
|
||||
albumUser: Mocked<RepositoryInterface<AlbumUserRepository>>;
|
||||
apiKey: Mocked<RepositoryInterface<ApiKeyRepository>>;
|
||||
audit: Mocked<RepositoryInterface<AuditRepository>>;
|
||||
asset: Mocked<IAssetRepository>;
|
||||
config: Mocked<RepositoryInterface<ConfigRepository>>;
|
||||
cron: Mocked<RepositoryInterface<CronRepository>>;
|
||||
crypto: Mocked<ICryptoRepository>;
|
||||
database: Mocked<RepositoryInterface<DatabaseRepository>>;
|
||||
event: Mocked<IEventRepository>;
|
||||
job: Mocked<RepositoryInterface<JobRepository>>;
|
||||
library: Mocked<RepositoryInterface<LibraryRepository>>;
|
||||
logger: Mocked<ILoggingRepository>;
|
||||
machineLearning: Mocked<IMachineLearningRepository>;
|
||||
map: Mocked<RepositoryInterface<MapRepository>>;
|
||||
media: Mocked<RepositoryInterface<MediaRepository>>;
|
||||
memory: Mocked<RepositoryInterface<MemoryRepository>>;
|
||||
metadata: Mocked<RepositoryInterface<MetadataRepository>>;
|
||||
move: Mocked<RepositoryInterface<MoveRepository>>;
|
||||
notification: Mocked<RepositoryInterface<NotificationRepository>>;
|
||||
oauth: Mocked<RepositoryInterface<OAuthRepository>>;
|
||||
partner: Mocked<RepositoryInterface<PartnerRepository>>;
|
||||
person: Mocked<RepositoryInterface<PersonRepository>>;
|
||||
process: Mocked<RepositoryInterface<ProcessRepository>>;
|
||||
search: Mocked<RepositoryInterface<SearchRepository>>;
|
||||
serverInfo: Mocked<RepositoryInterface<ServerInfoRepository>>;
|
||||
session: Mocked<RepositoryInterface<SessionRepository>>;
|
||||
sharedLink: Mocked<RepositoryInterface<SharedLinkRepository>>;
|
||||
stack: Mocked<RepositoryInterface<StackRepository>>;
|
||||
storage: Mocked<RepositoryInterface<StorageRepository>>;
|
||||
systemMetadata: Mocked<RepositoryInterface<SystemMetadataRepository>>;
|
||||
tag: Mocked<RepositoryInterface<TagRepository>>;
|
||||
telemetry: ITelemetryRepositoryMock;
|
||||
trash: Mocked<RepositoryInterface<TrashRepository>>;
|
||||
user: Mocked<IUserRepository>;
|
||||
versionHistory: Mocked<RepositoryInterface<VersionHistoryRepository>>;
|
||||
view: Mocked<RepositoryInterface<ViewRepository>>;
|
||||
};
|
||||
|
||||
export const newTestService = <T extends BaseService>(
|
||||
Service: Constructor<T, BaseServiceArgs>,
|
||||
overrides?: Overrides,
|
||||
|
@ -116,13 +158,15 @@ export const newTestService = <T extends BaseService>(
|
|||
const databaseMock = newDatabaseRepositoryMock();
|
||||
const eventMock = newEventRepositoryMock();
|
||||
const jobMock = newJobRepositoryMock();
|
||||
const keyMock = newKeyRepositoryMock();
|
||||
const apiKeyMock = newKeyRepositoryMock();
|
||||
const libraryMock = newLibraryRepositoryMock();
|
||||
const machineLearningMock = newMachineLearningRepositoryMock();
|
||||
const mapMock = newMapRepositoryMock();
|
||||
const mediaMock = newMediaRepositoryMock();
|
||||
const memoryMock = newMemoryRepositoryMock();
|
||||
const metadataMock = (metadataRepository || newMetadataRepositoryMock()) as Mocked<IMetadataRepository>;
|
||||
const metadataMock = (metadataRepository || newMetadataRepositoryMock()) as Mocked<
|
||||
RepositoryInterface<MetadataRepository>
|
||||
>;
|
||||
const moveMock = newMoveRepositoryMock();
|
||||
const notificationMock = newNotificationRepositoryMock();
|
||||
const oauthMock = newOAuthRepositoryMock();
|
||||
|
@ -146,86 +190,88 @@ export const newTestService = <T extends BaseService>(
|
|||
const sut = new Service(
|
||||
loggerMock as ILoggingRepository as LoggingRepository,
|
||||
accessMock as IAccessRepository as AccessRepository,
|
||||
activityMock as IActivityRepository as ActivityRepository,
|
||||
auditMock as IAuditRepository as AuditRepository,
|
||||
activityMock as RepositoryInterface<ActivityRepository> as ActivityRepository,
|
||||
auditMock as RepositoryInterface<AuditRepository> as AuditRepository,
|
||||
albumMock,
|
||||
albumUserMock as IAlbumUserRepository as AlbumUserRepository,
|
||||
albumUserMock as RepositoryInterface<AlbumUserRepository> as AlbumUserRepository,
|
||||
assetMock,
|
||||
configMock,
|
||||
cronMock as ICronRepository as CronRepository,
|
||||
cryptoMock,
|
||||
cronMock as RepositoryInterface<CronRepository> as CronRepository,
|
||||
cryptoMock as RepositoryInterface<CryptoRepository> as CryptoRepository,
|
||||
databaseMock,
|
||||
eventMock,
|
||||
jobMock,
|
||||
keyMock as IApiKeyRepository as ApiKeyRepository,
|
||||
apiKeyMock as RepositoryInterface<ApiKeyRepository> as ApiKeyRepository,
|
||||
libraryMock,
|
||||
machineLearningMock,
|
||||
mapMock as IMapRepository as MapRepository,
|
||||
mediaMock as IMediaRepository as MediaRepository,
|
||||
memoryMock as IMemoryRepository as MemoryRepository,
|
||||
metadataMock as IMetadataRepository as MetadataRepository,
|
||||
mapMock as RepositoryInterface<MapRepository> as MapRepository,
|
||||
mediaMock as RepositoryInterface<MediaRepository> as MediaRepository,
|
||||
memoryMock as RepositoryInterface<MemoryRepository> as MemoryRepository,
|
||||
metadataMock as RepositoryInterface<MetadataRepository> as MetadataRepository,
|
||||
moveMock,
|
||||
notificationMock as INotificationRepository as NotificationRepository,
|
||||
oauthMock as IOAuthRepository as OAuthRepository,
|
||||
notificationMock as RepositoryInterface<NotificationRepository> as NotificationRepository,
|
||||
oauthMock as RepositoryInterface<OAuthRepository> as OAuthRepository,
|
||||
partnerMock,
|
||||
personMock,
|
||||
processMock as IProcessRepository as ProcessRepository,
|
||||
processMock as RepositoryInterface<ProcessRepository> as ProcessRepository,
|
||||
searchMock,
|
||||
serverInfoMock as IServerInfoRepository as ServerInfoRepository,
|
||||
sessionMock as ISessionRepository as SessionRepository,
|
||||
serverInfoMock as RepositoryInterface<ServerInfoRepository> as ServerInfoRepository,
|
||||
sessionMock as RepositoryInterface<SessionRepository> as SessionRepository,
|
||||
sharedLinkMock,
|
||||
stackMock,
|
||||
storageMock,
|
||||
systemMock as ISystemMetadataRepository as SystemMetadataRepository,
|
||||
systemMock as RepositoryInterface<SystemMetadataRepository> as SystemMetadataRepository,
|
||||
tagMock,
|
||||
telemetryMock as unknown as TelemetryRepository,
|
||||
trashMock as ITrashRepository as TrashRepository,
|
||||
trashMock as RepositoryInterface<TrashRepository> as TrashRepository,
|
||||
userMock,
|
||||
versionHistoryMock as IVersionHistoryRepository as VersionHistoryRepository,
|
||||
viewMock as IViewRepository as ViewRepository,
|
||||
versionHistoryMock as RepositoryInterface<VersionHistoryRepository> as VersionHistoryRepository,
|
||||
viewMock as RepositoryInterface<ViewRepository> as ViewRepository,
|
||||
);
|
||||
|
||||
return {
|
||||
sut,
|
||||
accessMock,
|
||||
loggerMock,
|
||||
cronMock,
|
||||
cryptoMock,
|
||||
activityMock,
|
||||
auditMock,
|
||||
albumMock,
|
||||
albumUserMock,
|
||||
assetMock,
|
||||
configMock,
|
||||
databaseMock,
|
||||
eventMock,
|
||||
jobMock,
|
||||
keyMock,
|
||||
libraryMock,
|
||||
machineLearningMock,
|
||||
mapMock,
|
||||
mediaMock,
|
||||
memoryMock,
|
||||
metadataMock,
|
||||
moveMock,
|
||||
notificationMock,
|
||||
oauthMock,
|
||||
partnerMock,
|
||||
personMock,
|
||||
processMock,
|
||||
searchMock,
|
||||
serverInfoMock,
|
||||
sessionMock,
|
||||
sharedLinkMock,
|
||||
stackMock,
|
||||
storageMock,
|
||||
systemMock,
|
||||
tagMock,
|
||||
telemetryMock,
|
||||
trashMock,
|
||||
userMock,
|
||||
versionHistoryMock,
|
||||
viewMock,
|
||||
mocks: {
|
||||
access: accessMock,
|
||||
apiKey: apiKeyMock,
|
||||
cron: cronMock,
|
||||
crypto: cryptoMock,
|
||||
activity: activityMock,
|
||||
audit: auditMock,
|
||||
album: albumMock,
|
||||
albumUser: albumUserMock,
|
||||
asset: assetMock,
|
||||
config: configMock,
|
||||
database: databaseMock,
|
||||
event: eventMock,
|
||||
job: jobMock,
|
||||
library: libraryMock,
|
||||
logger: loggerMock,
|
||||
machineLearning: machineLearningMock,
|
||||
map: mapMock,
|
||||
media: mediaMock,
|
||||
memory: memoryMock,
|
||||
metadata: metadataMock,
|
||||
move: moveMock,
|
||||
notification: notificationMock,
|
||||
oauth: oauthMock,
|
||||
partner: partnerMock,
|
||||
person: personMock,
|
||||
process: processMock,
|
||||
search: searchMock,
|
||||
serverInfo: serverInfoMock,
|
||||
session: sessionMock,
|
||||
sharedLink: sharedLinkMock,
|
||||
stack: stackMock,
|
||||
storage: storageMock,
|
||||
systemMetadata: systemMock,
|
||||
tag: tagMock,
|
||||
telemetry: telemetryMock,
|
||||
trash: trashMock,
|
||||
user: userMock,
|
||||
versionHistory: versionHistoryMock,
|
||||
view: viewMock,
|
||||
} as ServiceMocks,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue