mirror of
https://github.com/immich-app/immich.git
synced 2026-02-12 11:58:15 +03:00
refactor: small tests (#26141)
This commit is contained in:
@@ -9,12 +9,15 @@ import { AssetFile } from 'src/database';
|
||||
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { UploadBody } from 'src/types';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { AssetFileFactory } from 'test/factories/asset-file.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
@@ -470,12 +473,13 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle a sidecar file', async () => {
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.asset.create.mockResolvedValueOnce(assetStub.image);
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
|
||||
mocks.asset.getById.mockResolvedValueOnce(asset);
|
||||
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||
|
||||
await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({
|
||||
status: AssetMediaStatus.CREATED,
|
||||
id: assetStub.image.id,
|
||||
id: asset.id,
|
||||
});
|
||||
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
@@ -501,13 +505,14 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should download a file', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getForOriginal.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForOriginal.mockResolvedValue(asset);
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', {})).resolves.toEqual(
|
||||
await expect(sut.downloadOriginal(authStub.admin, asset.id, {})).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/original/path.jpg',
|
||||
fileName: 'asset-id.jpg',
|
||||
path: asset.originalPath,
|
||||
fileName: asset.originalFileName,
|
||||
contentType: 'image/jpeg',
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
}),
|
||||
@@ -573,28 +578,16 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should not return the unedited version if requested using a shared link', async () => {
|
||||
const editedAsset = {
|
||||
...assetStub.withCropEdit,
|
||||
files: [
|
||||
...assetStub.withCropEdit.files,
|
||||
{
|
||||
id: 'edited-file',
|
||||
type: AssetFileType.FullSize,
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
isEdited: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getForOriginal.mockResolvedValue({
|
||||
...editedAsset,
|
||||
editedPath: '/uploads/user-id/fullsize/edited.jpg',
|
||||
});
|
||||
const fullsizeEdited = AssetFileFactory.create({ type: AssetFileType.FullSize, isEdited: true });
|
||||
const editedAsset = AssetFactory.from().edit({ action: AssetEditAction.Crop }).file(fullsizeEdited).build();
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.adminSharedLink, 'asset-id', { edited: false })).resolves.toEqual(
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([editedAsset.id]));
|
||||
mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: fullsizeEdited.path });
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.adminSharedLink, editedAsset.id, { edited: false })).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
fileName: 'asset-id.jpg',
|
||||
path: fullsizeEdited.path,
|
||||
fileName: editedAsset.originalFileName,
|
||||
contentType: 'image/jpeg',
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
}),
|
||||
@@ -638,129 +631,118 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should fall back to preview if the requested thumbnail file does not exist', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/path/to/preview.jpg' });
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||
).resolves.toEqual(
|
||||
await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/path/to/preview.jpg',
|
||||
path: asset.files[0].path,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'asset-id_thumbnail.jpg',
|
||||
fileName: `IMG_${asset.id}_thumbnail.jpg`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should get preview file', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/uploads/user-id/thumbs/path.jpg' });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
||||
).resolves.toEqual(
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||
await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.PREVIEW })).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/thumbs/path.jpg',
|
||||
path: asset.files[0].path,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'asset-id_preview.jpg',
|
||||
fileName: `IMG_${asset.id}_preview.jpg`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should get thumbnail file', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/uploads/user-id/webp/path.ext' });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||
).resolves.toEqual(
|
||||
const asset = AssetFactory.from()
|
||||
.file({ type: AssetFileType.Thumbnail, path: '/uploads/user-id/webp/path.ext' })
|
||||
.build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||
await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/webp/path.ext',
|
||||
path: asset.files[0].path,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'application/octet-stream',
|
||||
fileName: 'asset-id_thumbnail.ext',
|
||||
fileName: `IMG_${asset.id}_thumbnail.ext`,
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false);
|
||||
});
|
||||
|
||||
it('should get original thumbnail by default', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
path: '/uploads/user-id/thumbs/original-thumbnail.jpg',
|
||||
});
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||
).resolves.toEqual(
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||
await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/thumbs/original-thumbnail.jpg',
|
||||
path: asset.files[0].path,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'asset-id_thumbnail.jpg',
|
||||
fileName: `IMG_${asset.id}_thumbnail.jpg`,
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false);
|
||||
});
|
||||
|
||||
it('should get edited thumbnail when edited=true', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
path: '/uploads/user-id/thumbs/edited-thumbnail.jpg',
|
||||
});
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail, isEdited: true }).build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL, edited: true }),
|
||||
sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL, edited: true }),
|
||||
).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/thumbs/edited-thumbnail.jpg',
|
||||
path: asset.files[0].path,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'asset-id_thumbnail.jpg',
|
||||
fileName: `IMG_${asset.id}_thumbnail.jpg`,
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, true);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, true);
|
||||
});
|
||||
|
||||
it('should get original thumbnail when edited=false', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
path: '/uploads/user-id/thumbs/original-thumbnail.jpg',
|
||||
});
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL, edited: false }),
|
||||
sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL, edited: false }),
|
||||
).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/thumbs/original-thumbnail.jpg',
|
||||
path: asset.files[0].path,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'asset-id_thumbnail.jpg',
|
||||
fileName: `IMG_${asset.id}_thumbnail.jpg`,
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false);
|
||||
});
|
||||
|
||||
it('should not return the unedited version if requested using a shared link', async () => {
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
path: '/uploads/user-id/thumbs/edited-thumbnail.jpg',
|
||||
});
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build();
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.adminSharedLink, assetStub.image.id, {
|
||||
sut.viewThumbnail(authStub.adminSharedLink, asset.id, {
|
||||
size: AssetMediaSize.THUMBNAIL,
|
||||
edited: true,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/thumbs/edited-thumbnail.jpg',
|
||||
path: asset.files[0].path,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'asset-id_thumbnail.jpg',
|
||||
fileName: `IMG_${asset.id}_thumbnail.jpg`,
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, true);
|
||||
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -774,18 +756,20 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should throw an error if the video asset could not be found', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException);
|
||||
await expect(sut.playbackVideo(authStub.admin, asset.id)).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should return the encoded video path if available', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id]));
|
||||
mocks.asset.getForVideo.mockResolvedValue(assetStub.hasEncodedVideo);
|
||||
const asset = AssetFactory.create({ encodedVideoPath: '/path/to/encoded/video.mp4' });
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForVideo.mockResolvedValue(asset);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual(
|
||||
await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: assetStub.hasEncodedVideo.encodedVideoPath!,
|
||||
path: asset.encodedVideoPath!,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'video/mp4',
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DateTime } from 'luxon';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||
import { AssetFileType, AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||
import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
@@ -79,7 +79,7 @@ describe(AssetService.name, () => {
|
||||
describe('getRandom', () => {
|
||||
it('should get own random assets', async () => {
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
||||
|
||||
await sut.getRandom(authStub.admin, 1);
|
||||
|
||||
@@ -90,7 +90,7 @@ describe(AssetService.name, () => {
|
||||
const partner = factory.partner({ inTimeline: false });
|
||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||
|
||||
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
||||
|
||||
await sut.getRandom(auth, 1);
|
||||
@@ -102,7 +102,7 @@ describe(AssetService.name, () => {
|
||||
const partner = factory.partner({ inTimeline: true });
|
||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||
|
||||
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
||||
|
||||
await sut.getRandom(auth, 1);
|
||||
@@ -113,88 +113,90 @@ describe(AssetService.name, () => {
|
||||
|
||||
describe('get', () => {
|
||||
it('should allow owner access', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
|
||||
await sut.get(authStub.admin, assetStub.image.id);
|
||||
await sut.get(authStub.admin, asset.id);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
new Set([asset.id]),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow shared link access', async () => {
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
|
||||
await sut.get(authStub.adminSharedLink, assetStub.image.id);
|
||||
await sut.get(authStub.adminSharedLink, asset.id);
|
||||
|
||||
expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.sharedLink?.id,
|
||||
new Set([assetStub.image.id]),
|
||||
new Set([asset.id]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip metadata for shared link if exif is disabled', async () => {
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.from().exif({ description: 'foo' }).build();
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
|
||||
const result = await sut.get(
|
||||
{ ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
|
||||
assetStub.image.id,
|
||||
asset.id,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ hasMetadata: false }));
|
||||
expect(result).not.toHaveProperty('exifInfo');
|
||||
expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.sharedLink?.id,
|
||||
new Set([assetStub.image.id]),
|
||||
new Set([asset.id]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow partner sharing access', async () => {
|
||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
|
||||
await sut.get(authStub.admin, assetStub.image.id);
|
||||
await sut.get(authStub.admin, asset.id);
|
||||
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([asset.id]));
|
||||
});
|
||||
|
||||
it('should allow shared album access', async () => {
|
||||
mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
|
||||
await sut.get(authStub.admin, assetStub.image.id);
|
||||
await sut.get(authStub.admin, asset.id);
|
||||
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
);
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([asset.id]));
|
||||
});
|
||||
|
||||
it('should throw an error for no access', async () => {
|
||||
await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.get(authStub.admin, AssetFactory.create().id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
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);
|
||||
await expect(sut.get(authStub.adminSharedLink, AssetFactory.create().id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
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 () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
|
||||
await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.get(authStub.admin, asset.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,38 +210,41 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should update the asset', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.update.mockResolvedValue(asset);
|
||||
|
||||
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
|
||||
await sut.update(authStub.admin, asset.id, { isFavorite: true });
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, isFavorite: true });
|
||||
});
|
||||
|
||||
it('should update the exif description', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.update.mockResolvedValue(asset);
|
||||
|
||||
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
|
||||
await sut.update(authStub.admin, asset.id, { description: 'Test description' });
|
||||
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
{ assetId: 'asset-1', description: 'Test description', lockedProperties: ['description'] },
|
||||
{ assetId: asset.id, description: 'Test description', lockedProperties: ['description'] },
|
||||
{ lockedPropertiesBehavior: 'append' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should update the exif rating', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValueOnce(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(asset);
|
||||
mocks.asset.update.mockResolvedValueOnce(asset);
|
||||
|
||||
await sut.update(authStub.admin, 'asset-1', { rating: 3 });
|
||||
await sut.update(authStub.admin, asset.id, { rating: 3 });
|
||||
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
{
|
||||
assetId: 'asset-1',
|
||||
assetId: asset.id,
|
||||
rating: 3,
|
||||
lockedProperties: ['rating'],
|
||||
},
|
||||
@@ -346,10 +351,11 @@ describe(AssetService.name, () => {
|
||||
|
||||
it('should unlink a live video', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const asset = AssetFactory.create();
|
||||
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);
|
||||
mocks.asset.update.mockResolvedValueOnce(asset);
|
||||
|
||||
await sut.update(auth, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
|
||||
|
||||
@@ -555,7 +561,11 @@ describe(AssetService.name, () => {
|
||||
|
||||
describe('handleAssetDeletion', () => {
|
||||
it('should clean up files', async () => {
|
||||
const asset = assetStub.image;
|
||||
const asset = AssetFactory.from()
|
||||
.file({ type: AssetFileType.Thumbnail })
|
||||
.file({ type: AssetFileType.Preview })
|
||||
.file({ type: AssetFileType.FullSize })
|
||||
.build();
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
@@ -565,12 +575,7 @@ describe(AssetService.name, () => {
|
||||
{
|
||||
name: JobName.FileDelete,
|
||||
data: {
|
||||
files: [
|
||||
'/uploads/user-id/webp/path.ext',
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
'/uploads/user-id/fullsize/path.webp',
|
||||
asset.originalPath,
|
||||
],
|
||||
files: [...asset.files.map(({ path }) => path), asset.originalPath],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -656,14 +661,15 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should update usage', async () => {
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.image);
|
||||
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
||||
const asset = AssetFactory.from().exif({ fileSizeInByte: 5000 }).build();
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, -5000);
|
||||
});
|
||||
|
||||
it('should fail if asset could not be found', async () => {
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(void 0);
|
||||
await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe(
|
||||
await expect(sut.handleAssetDeletion({ id: AssetFactory.create().id, deleteOnDisk: true })).resolves.toBe(
|
||||
JobStatus.Failed,
|
||||
);
|
||||
});
|
||||
@@ -681,28 +687,30 @@ describe(AssetService.name, () => {
|
||||
it('should return OCR data for an asset', async () => {
|
||||
const ocr1 = factory.assetOcr({ text: 'Hello World' });
|
||||
const ocr2 = factory.assetOcr({ text: 'Test Image' });
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([ocr1, ocr2]);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
|
||||
await expect(sut.getOcr(authStub.admin, 'asset-1')).resolves.toEqual([ocr1, ocr2]);
|
||||
await expect(sut.getOcr(authStub.admin, asset.id)).resolves.toEqual([ocr1, ocr2]);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set(['asset-1']),
|
||||
new Set([asset.id]),
|
||||
undefined,
|
||||
);
|
||||
expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith('asset-1');
|
||||
expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith(asset.id);
|
||||
});
|
||||
|
||||
it('should return empty array when no OCR data exists', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
await expect(sut.getOcr(authStub.admin, 'asset-1')).resolves.toEqual([]);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
await expect(sut.getOcr(authStub.admin, asset.id)).resolves.toEqual([]);
|
||||
|
||||
expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith('asset-1');
|
||||
expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith(asset.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -746,7 +754,7 @@ describe(AssetService.name, () => {
|
||||
|
||||
describe('getUserAssetsByDeviceId', () => {
|
||||
it('get assets by device id', async () => {
|
||||
const assets = [assetStub.image, assetStub.image1];
|
||||
const assets = [AssetFactory.create(), AssetFactory.create()];
|
||||
|
||||
mocks.asset.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId));
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Readable } from 'node:stream';
|
||||
import { DownloadResponseDto } from 'src/dtos/download.dto';
|
||||
import { DownloadService } from 'src/services/download.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
import { vitest } from 'vitest';
|
||||
@@ -37,21 +36,18 @@ describe(DownloadService.name, () => {
|
||||
finalize: vitest.fn(),
|
||||
stream: new Readable(),
|
||||
};
|
||||
const asset = AssetFactory.create();
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.noResizePath, id: 'asset-1' }]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id, 'unknown-asset']));
|
||||
mocks.asset.getByIds.mockResolvedValue([asset]);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id, 'unknown-asset'] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
});
|
||||
|
||||
expect(archiveMock.addFile).toHaveBeenCalledTimes(1);
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.stringContaining('/data/library/IMG_123.jpg'),
|
||||
'IMG_123.jpg',
|
||||
);
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset.originalPath, asset.originalFileName);
|
||||
});
|
||||
|
||||
it('should log a warning if the original path could not be resolved', async () => {
|
||||
@@ -108,15 +104,14 @@ describe(DownloadService.name, () => {
|
||||
finalize: vitest.fn(),
|
||||
stream: new Readable(),
|
||||
};
|
||||
const asset1 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' });
|
||||
const asset2 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' });
|
||||
|
||||
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' },
|
||||
]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
|
||||
mocks.asset.getByIds.mockResolvedValue([asset1, asset2]);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
});
|
||||
|
||||
@@ -131,15 +126,14 @@ describe(DownloadService.name, () => {
|
||||
finalize: vitest.fn(),
|
||||
stream: new Readable(),
|
||||
};
|
||||
const asset1 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' });
|
||||
const asset2 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' });
|
||||
|
||||
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' },
|
||||
]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
|
||||
mocks.asset.getByIds.mockResolvedValue([asset2, asset1]);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
});
|
||||
|
||||
@@ -155,18 +149,17 @@ describe(DownloadService.name, () => {
|
||||
stream: new Readable(),
|
||||
};
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{ ...assetStub.noResizePath, id: 'asset-1', originalPath: '/path/to/symlink.jpg' },
|
||||
]);
|
||||
const asset = AssetFactory.create({ originalPath: '/path/to/symlink.jpg' });
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getByIds.mockResolvedValue([asset]);
|
||||
mocks.storage.realpath.mockResolvedValue('/path/to/realpath.jpg');
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1'] })).resolves.toEqual({
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
});
|
||||
|
||||
expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', 'IMG_123.jpg');
|
||||
expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', asset.originalFileName);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||
import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -38,19 +39,17 @@ describe(SearchService.name, () => {
|
||||
|
||||
describe('getDuplicates', () => {
|
||||
it('should get duplicates', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.duplicateRepository.getAll.mockResolvedValue([
|
||||
{
|
||||
duplicateId: 'duplicate-id',
|
||||
assets: [assetStub.image, assetStub.image],
|
||||
assets: [asset, asset],
|
||||
},
|
||||
]);
|
||||
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
|
||||
{
|
||||
duplicateId: 'duplicate-id',
|
||||
assets: [
|
||||
expect.objectContaining({ id: assetStub.image.id }),
|
||||
expect.objectContaining({ id: assetStub.image.id }),
|
||||
],
|
||||
assets: [expect.objectContaining({ id: asset.id }), expect.objectContaining({ id: asset.id })],
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -101,7 +100,8 @@ describe(SearchService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue missing assets', async () => {
|
||||
mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueSearchDuplicates({});
|
||||
|
||||
@@ -109,13 +109,14 @@ describe(SearchService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetDetectDuplicates,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should queue all assets', async () => {
|
||||
mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueSearchDuplicates({ force: true });
|
||||
|
||||
@@ -123,7 +124,7 @@ describe(SearchService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetDetectDuplicates,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -178,10 +179,11 @@ describe(SearchService.name, () => {
|
||||
it('should fail if asset is not found', async () => {
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue(void 0);
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id: assetStub.image.id });
|
||||
const asset = AssetFactory.create();
|
||||
const result = await sut.handleSearchDuplicates({ id: asset.id });
|
||||
|
||||
expect(result).toBe(JobStatus.Failed);
|
||||
expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`);
|
||||
expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${asset.id} not found`);
|
||||
});
|
||||
|
||||
it('should skip if asset is part of stack', async () => {
|
||||
@@ -210,19 +212,19 @@ describe(SearchService.name, () => {
|
||||
it('should fail if asset is missing embedding', async () => {
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, embedding: null });
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id: assetStub.image.id });
|
||||
const asset = AssetFactory.create();
|
||||
const result = await sut.handleSearchDuplicates({ id: asset.id });
|
||||
|
||||
expect(result).toBe(JobStatus.Failed);
|
||||
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${assetStub.image.id} is missing embedding`);
|
||||
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is missing embedding`);
|
||||
});
|
||||
|
||||
it('should search for duplicates and update asset with duplicateId', async () => {
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue(hasEmbedding);
|
||||
mocks.duplicateRepository.search.mockResolvedValue([
|
||||
{ assetId: assetStub.image.id, distance: 0.01, duplicateId: null },
|
||||
]);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.duplicateRepository.search.mockResolvedValue([{ assetId: asset.id, distance: 0.01, duplicateId: null }]);
|
||||
mocks.duplicateRepository.merge.mockResolvedValue();
|
||||
const expectedAssetIds = [assetStub.image.id, hasEmbedding.id];
|
||||
const expectedAssetIds = [asset.id, hasEmbedding.id];
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id });
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { JobItem } from 'src/types';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@@ -55,7 +56,7 @@ describe(JobService.name, () => {
|
||||
{
|
||||
item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } },
|
||||
jobs: [],
|
||||
stub: [assetStub.image],
|
||||
stub: [AssetFactory.create({ id: 'asset-id' })],
|
||||
},
|
||||
{
|
||||
item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } },
|
||||
|
||||
@@ -6,6 +6,7 @@ import { mapLibrary } from 'src/dtos/library.dto';
|
||||
import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
@@ -548,13 +549,14 @@ describe(LibraryService.name, () => {
|
||||
|
||||
it('should import a new asset', async () => {
|
||||
const library = factory.library();
|
||||
const asset = AssetFactory.create();
|
||||
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
libraryId: library.id,
|
||||
paths: ['/data/user1/photo.jpg'],
|
||||
};
|
||||
|
||||
mocks.asset.createAll.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.createAll.mockResolvedValue([asset]);
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
|
||||
await expect(sut.handleSyncFiles(mockLibraryJob)).resolves.toBe(JobStatus.Success);
|
||||
@@ -575,7 +577,7 @@ describe(LibraryService.name, () => {
|
||||
{
|
||||
name: JobName.SidecarCheck,
|
||||
data: {
|
||||
id: assetStub.image.id,
|
||||
id: asset.id,
|
||||
source: 'upload',
|
||||
},
|
||||
},
|
||||
@@ -602,7 +604,7 @@ describe(LibraryService.name, () => {
|
||||
it('should delete a library', async () => {
|
||||
const library = factory.library();
|
||||
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create());
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
|
||||
await sut.delete(library.id);
|
||||
@@ -614,7 +616,7 @@ describe(LibraryService.name, () => {
|
||||
it('should allow an external library to be deleted', async () => {
|
||||
const library = factory.library();
|
||||
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create());
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
|
||||
await sut.delete(library.id);
|
||||
@@ -630,7 +632,7 @@ describe(LibraryService.name, () => {
|
||||
it('should unwatch an external library when deleted', async () => {
|
||||
const library = factory.library({ importPaths: ['/foo', '/bar'] });
|
||||
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create());
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.library.getAll.mockResolvedValue([library]);
|
||||
|
||||
@@ -962,7 +964,7 @@ describe(LibraryService.name, () => {
|
||||
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.library.getAll.mockResolvedValue([library]);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create());
|
||||
mocks.storage.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
|
||||
|
||||
await sut.watchAll();
|
||||
@@ -981,7 +983,7 @@ describe(LibraryService.name, () => {
|
||||
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.library.getAll.mockResolvedValue([library]);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create());
|
||||
mocks.storage.watch.mockImplementation(
|
||||
makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }),
|
||||
);
|
||||
@@ -999,12 +1001,13 @@ describe(LibraryService.name, () => {
|
||||
|
||||
it('should handle a file unlink event', async () => {
|
||||
const library = factory.library({ importPaths: ['/foo', '/bar'] });
|
||||
const asset = AssetFactory.create();
|
||||
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.library.getAll.mockResolvedValue([library]);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(asset);
|
||||
mocks.storage.watch.mockImplementation(
|
||||
makeMockWatcher({ items: [{ event: 'unlink', value: assetStub.image.originalPath }] }),
|
||||
makeMockWatcher({ items: [{ event: 'unlink', value: asset.originalPath }] }),
|
||||
);
|
||||
|
||||
await sut.watchAll();
|
||||
@@ -1013,7 +1016,7 @@ describe(LibraryService.name, () => {
|
||||
name: JobName.LibraryRemoveAsset,
|
||||
data: {
|
||||
libraryId: library.id,
|
||||
paths: [assetStub.image.originalPath],
|
||||
paths: [asset.originalPath],
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1115,7 +1118,7 @@ describe(LibraryService.name, () => {
|
||||
const library = factory.library();
|
||||
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.image1]));
|
||||
mocks.library.streamAssetIds.mockReturnValue(makeStream([AssetFactory.create()]));
|
||||
|
||||
await expect(sut.handleDeleteLibrary({ id: library.id })).resolves.toBe(JobStatus.Success);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OutputInfo } from 'sharp';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { Exif } from 'src/database';
|
||||
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import {
|
||||
AssetFileType,
|
||||
AssetPathType,
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
import { MediaService } from 'src/services/media.service';
|
||||
import { JobCounts, RawImageInfo } from 'src/types';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub, previewFile } from 'test/fixtures/asset.stub';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personStub, personThumbnailStub } from 'test/fixtures/person.stub';
|
||||
@@ -45,7 +46,8 @@ describe(MediaService.name, () => {
|
||||
|
||||
describe('handleQueueGenerateThumbnails', () => {
|
||||
it('should queue all assets', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||
|
||||
mocks.person.getAll.mockReturnValue(makeStream([personStub.newThumbnail]));
|
||||
|
||||
@@ -55,7 +57,7 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetGenerateThumbnails,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -99,7 +101,7 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue all people with missing thumbnail path', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([AssetFactory.create()]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream([personStub.noThumbnail, personStub.noThumbnail]));
|
||||
mocks.person.getRandomFace.mockResolvedValueOnce(faceStub.face1);
|
||||
|
||||
@@ -120,7 +122,8 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue all assets with missing resize path', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noResizePath]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
@@ -128,7 +131,7 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetGenerateThumbnails,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -264,16 +267,15 @@ describe(MediaService.name, () => {
|
||||
|
||||
describe('handleQueueMigration', () => {
|
||||
it('should remove empty directories and queue jobs', async () => {
|
||||
mocks.assetJob.streamForMigrationJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForMigrationJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts);
|
||||
mocks.person.getAll.mockReturnValue(makeStream([personStub.withName]));
|
||||
|
||||
await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.storage.removeEmptyDirs).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.AssetFileMigration, data: { id: assetStub.image.id } },
|
||||
]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.AssetFileMigration, data: { id: asset.id } }]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.PersonFileMigration, data: { id: personStub.withName.id } },
|
||||
]);
|
||||
@@ -283,39 +285,42 @@ describe(MediaService.name, () => {
|
||||
describe('handleAssetMigration', () => {
|
||||
it('should fail if asset does not exist', async () => {
|
||||
mocks.assetJob.getForMigrationJob.mockResolvedValue(void 0);
|
||||
await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Failed);
|
||||
await expect(sut.handleAssetMigration({ id: 'non-existent' })).resolves.toBe(JobStatus.Failed);
|
||||
|
||||
expect(mocks.move.getByEntity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should move asset files', async () => {
|
||||
mocks.assetJob.getForMigrationJob.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.from()
|
||||
.files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail])
|
||||
.build();
|
||||
mocks.assetJob.getForMigrationJob.mockResolvedValue(asset);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
entityId: assetStub.image.id,
|
||||
entityId: asset.id,
|
||||
id: 'move-id',
|
||||
newPath: '/new/path',
|
||||
oldPath: '/old/path',
|
||||
pathType: AssetPathType.Original,
|
||||
});
|
||||
|
||||
await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success);
|
||||
await expect(sut.handleAssetMigration({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: assetStub.image.id,
|
||||
entityId: asset.id,
|
||||
pathType: AssetFileType.FullSize,
|
||||
oldPath: '/uploads/user-id/fullsize/path.webp',
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_fullsize.jpeg'),
|
||||
oldPath: asset.files[0].path,
|
||||
newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_fullsize.jpeg`,
|
||||
});
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: assetStub.image.id,
|
||||
entityId: asset.id,
|
||||
pathType: AssetFileType.Preview,
|
||||
oldPath: '/uploads/user-id/thumbs/path.jpg',
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_preview.jpeg'),
|
||||
oldPath: asset.files[1].path,
|
||||
newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.jpeg`,
|
||||
});
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: assetStub.image.id,
|
||||
entityId: asset.id,
|
||||
pathType: AssetFileType.Thumbnail,
|
||||
oldPath: '/uploads/user-id/webp/path.ext',
|
||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_thumbnail.webp'),
|
||||
oldPath: asset.files[2].path,
|
||||
newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.webp`,
|
||||
});
|
||||
expect(mocks.move.create).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
@@ -339,16 +344,17 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should skip thumbnail generation if asset not found', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(void 0);
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: 'non-existent' });
|
||||
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should skip thumbnail generation if asset type is unknown', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ ...assetStub.image, type: 'foo' as AssetType });
|
||||
const asset = AssetFactory.create({ type: 'foo' as AssetType });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await expect(sut.handleGenerateThumbnails({ id: assetStub.image.id })).resolves.toBe(JobStatus.Skipped);
|
||||
await expect(sut.handleGenerateThumbnails({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
|
||||
expect(mocks.media.probe).not.toHaveBeenCalled();
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith();
|
||||
@@ -372,33 +378,35 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should delete previous preview if different path', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.FileDelete,
|
||||
data: {
|
||||
files: expect.arrayContaining([previewFile.path]),
|
||||
files: expect.arrayContaining([asset.files[0].path]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate P3 thumbnails for a wide gamut image', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as Exif,
|
||||
});
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ profileDescription: 'Adobe RGB', bitsPerSample: 14 })
|
||||
.files([AssetFileType.Preview, AssetFileType.Thumbnail])
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
@@ -444,21 +452,21 @@ describe(MediaService.name, () => {
|
||||
|
||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
||||
{
|
||||
assetId: 'asset-id',
|
||||
assetId: asset.id,
|
||||
type: AssetFileType.Preview,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
isProgressive: false,
|
||||
},
|
||||
{
|
||||
assetId: 'asset-id',
|
||||
assetId: asset.id,
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: expect.any(String),
|
||||
isEdited: false,
|
||||
isProgressive: false,
|
||||
},
|
||||
]);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, thumbhash: thumbhashBuffer });
|
||||
});
|
||||
|
||||
it('should generate a thumbnail for a video', async () => {
|
||||
@@ -618,18 +626,19 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it.each(Object.values(ImageFormat))('should generate an image preview in %s format', async (format) => {
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { format } } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
const previewPath = `/data/thumbs/user-id/as/se/asset-id_preview.${format}`;
|
||||
const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id_thumbnail.webp`;
|
||||
const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.${format}`;
|
||||
const thumbnailPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.webp`;
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.Srgb,
|
||||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
@@ -667,18 +676,19 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it.each(Object.values(ImageFormat))('should generate an image thumbnail in %s format', async (format) => {
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format } } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_preview.jpeg`);
|
||||
const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_thumbnail.${format}`);
|
||||
const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.jpeg`;
|
||||
const thumbnailPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.${format}`;
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.Srgb,
|
||||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
@@ -716,12 +726,13 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate progressive JPEG for preview when enabled', async () => {
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { preview: { progressive: true }, thumbnail: { progressive: false } },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
@@ -752,12 +763,13 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate progressive JPEG for thumbnail when enabled', async () => {
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { preview: { progressive: false }, thumbnail: { format: ImageFormat.Jpeg, progressive: true } },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
@@ -809,26 +821,30 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should delete previous thumbnail if different path', async () => {
|
||||
const asset = AssetFactory.from().exif().file({ type: AssetFileType.Preview }).build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.FileDelete,
|
||||
data: {
|
||||
files: expect.arrayContaining([previewFile.path]),
|
||||
files: expect.arrayContaining([asset.files[0].path]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract embedded image if enabled and available', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' })
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, {
|
||||
@@ -839,14 +855,17 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should resize original image if embedded image is too small', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' })
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
@@ -854,13 +873,16 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should resize original image if embedded image not found', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' })
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
@@ -868,14 +890,17 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should resize original image if embedded image extraction is not enabled', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' })
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.extract).not.toHaveBeenCalled();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
@@ -884,14 +909,17 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should process invalid images if enabled', async () => {
|
||||
vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true');
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' })
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(
|
||||
assetStub.imageDng.originalPath,
|
||||
asset.originalPath,
|
||||
expect.objectContaining({ processInvalidImages: true }),
|
||||
);
|
||||
|
||||
@@ -917,14 +945,18 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should extract full-size JPEG preview from RAW', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' })
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true },
|
||||
});
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, {
|
||||
@@ -951,14 +983,18 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should convert full-size WEBP preview from JXL preview of RAW', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' })
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true },
|
||||
});
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, {
|
||||
@@ -997,15 +1033,19 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' })
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
processInvalidImages: false,
|
||||
});
|
||||
@@ -1079,15 +1119,16 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip generating full-size preview for web-friendly images', async () => {
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.Srgb,
|
||||
processInvalidImages: false,
|
||||
size: 1440,
|
||||
@@ -1116,7 +1157,7 @@ describe(MediaService.name, () => {
|
||||
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
@@ -1161,7 +1202,7 @@ describe(MediaService.name, () => {
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
@@ -1238,15 +1279,23 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should upsert 3 edited files for edit jobs', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||
...assetStub.withCropEdit,
|
||||
});
|
||||
const asset = AssetFactory.from()
|
||||
.exif()
|
||||
.edit({ action: AssetEditAction.Crop })
|
||||
.files([
|
||||
{ type: AssetFileType.FullSize, isEdited: true },
|
||||
{ type: AssetFileType.Preview, isEdited: true },
|
||||
{ type: AssetFileType.Thumbnail, isEdited: true },
|
||||
])
|
||||
.build();
|
||||
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
||||
|
||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
@@ -1258,21 +1307,23 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should apply edits when generating thumbnails', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||
...assetStub.withCropEdit,
|
||||
});
|
||||
const asset = AssetFactory.from()
|
||||
.exif()
|
||||
.edit({ action: AssetEditAction.Crop, parameters: { height: 1152, width: 1512, x: 216, y: 1512 } })
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
rawBuffer,
|
||||
expect.objectContaining({
|
||||
edits: [
|
||||
{
|
||||
expect.objectContaining({
|
||||
action: 'crop',
|
||||
parameters: { height: 1152, width: 1512, x: 216, y: 1512 },
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
expect.any(String),
|
||||
@@ -1305,13 +1356,12 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate all 3 edited files if an asset has edits', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||
...assetStub.withCropEdit,
|
||||
});
|
||||
const asset = AssetFactory.from().exif().edit().build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
@@ -1336,21 +1386,20 @@ describe(MediaService.name, () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.media.generateThumbhash.mockResolvedValue(factory.buffer());
|
||||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id, source: 'upload' });
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: asset.id, source: 'upload' });
|
||||
|
||||
expect(mocks.media.generateThumbhash).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should apply thumbhash if job source is edit and edits exist', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||
...assetStub.withCropEdit,
|
||||
});
|
||||
const asset = AssetFactory.from().exif().edit().build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
const thumbhashBuffer = factory.buffer();
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ thumbhash: thumbhashBuffer }));
|
||||
});
|
||||
|
||||
@@ -125,27 +125,29 @@ describe(MetadataService.name, () => {
|
||||
|
||||
describe('handleQueueMetadataExtraction', () => {
|
||||
it('should queue metadata extraction for all assets without exif values', async () => {
|
||||
mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(false);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetExtractMetadata,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should queue metadata extraction for all assets', async () => {
|
||||
mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(true);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetExtractMetadata,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -166,9 +168,9 @@ describe(MetadataService.name, () => {
|
||||
it('should handle an asset that could not be found', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(void 0);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: 'non-existent' });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith('non-existent');
|
||||
expect(mocks.asset.upsertExif).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -287,8 +289,8 @@ describe(MetadataService.name, () => {
|
||||
} as Stats);
|
||||
mockReadTags({ ISO: [160] });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }), {
|
||||
lockedPropertiesBehavior: 'skip',
|
||||
});
|
||||
@@ -406,7 +408,7 @@ describe(MetadataService.name, () => {
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||
userId: asset.ownerId,
|
||||
@@ -546,57 +548,59 @@ describe(MetadataService.name, () => {
|
||||
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: '2024', parent: undefined });
|
||||
});
|
||||
|
||||
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Mom|Dad'] }) });
|
||||
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({
|
||||
userId: 'user-id',
|
||||
userId: asset.ownerId,
|
||||
value: 'Mom|Dad',
|
||||
parent: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent/Child', 'Parent2/Child2'] }),
|
||||
});
|
||||
const baseAsset = AssetFactory.from();
|
||||
const asset = baseAsset.build();
|
||||
const updatedAsset = baseAsset.exif({ tags: ['Parent/Child', 'Parent2/Child2'] }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(updatedAsset);
|
||||
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||
userId: 'user-id',
|
||||
userId: asset.ownerId,
|
||||
value: 'Parent',
|
||||
parentId: undefined,
|
||||
});
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||
userId: 'user-id',
|
||||
userId: asset.ownerId,
|
||||
value: 'Parent/Child',
|
||||
parentId: 'tag-parent',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove existing tags', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.tag.replaceAssetTags).toHaveBeenCalledWith('asset-id', []);
|
||||
expect(mocks.tag.replaceAssetTags).toHaveBeenCalledWith(asset.id, []);
|
||||
});
|
||||
|
||||
it('should not apply motion photos if asset is video', async () => {
|
||||
@@ -617,13 +621,14 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle an invalid Directory Item', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({
|
||||
MotionPhoto: 1,
|
||||
ContainerDirectory: [{ Foo: 100 }],
|
||||
});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
});
|
||||
|
||||
it('should extract the correct video orientation', async () => {
|
||||
@@ -915,6 +920,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should save all metadata', async () => {
|
||||
const dateForTest = new Date('1970-01-01T00:00:00.000-11:30');
|
||||
const asset = AssetFactory.create();
|
||||
|
||||
const tags: ImmichTags = {
|
||||
BitsPerSample: 1,
|
||||
@@ -941,14 +947,14 @@ describe(MetadataService.name, () => {
|
||||
Rating: 3,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags(tags);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
{
|
||||
assetId: assetStub.image.id,
|
||||
assetId: asset.id,
|
||||
bitsPerSample: expect.any(Number),
|
||||
autoStackId: null,
|
||||
colorspace: tags.ColorSpace,
|
||||
@@ -983,7 +989,7 @@ describe(MetadataService.name, () => {
|
||||
);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: assetStub.image.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
fileCreatedAt: dateForTest,
|
||||
localDateTime: DateTime.fromISO('1970-01-01T00:00:00.000Z').toJSDate(),
|
||||
@@ -996,6 +1002,7 @@ describe(MetadataService.name, () => {
|
||||
// https://github.com/photostructure/exiftool-vendored.js/issues/203
|
||||
|
||||
// this only tests our assumptions of exiftool-vendored, demonstrating the issue
|
||||
const asset = AssetFactory.create();
|
||||
const someDate = '2024-09-01T00:00:00.000';
|
||||
expect(ExifDateTime.fromISO(someDate + 'Z')?.zone).toBe('UTC');
|
||||
expect(ExifDateTime.fromISO(someDate + '+00:00')?.zone).toBe('UTC'); // this is the issue, should be UTC+0
|
||||
@@ -1005,11 +1012,11 @@ describe(MetadataService.name, () => {
|
||||
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
|
||||
tz: undefined,
|
||||
};
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags(tags);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
timeZone: 'UTC+0',
|
||||
@@ -1034,14 +1041,15 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: assetStub.image.id,
|
||||
id: assetStub.video.id,
|
||||
duration: '00:00:06.210',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should only extract duration for videos', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
format: {
|
||||
@@ -1049,13 +1057,13 @@ describe(MetadataService.name, () => {
|
||||
duration: 6.21,
|
||||
},
|
||||
});
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: assetStub.image.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
}),
|
||||
);
|
||||
@@ -1077,7 +1085,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: assetStub.image.id,
|
||||
id: assetStub.video.id,
|
||||
duration: null,
|
||||
}),
|
||||
);
|
||||
@@ -1106,45 +1114,34 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should use Duration from exif', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
originalPath: '/original/path.webp',
|
||||
});
|
||||
const asset = AssetFactory.create({ originalFileName: 'file.webp' });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Duration: 123 }, {});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
});
|
||||
|
||||
it('should prefer Duration from exif over sidecar', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
originalPath: '/original/path.webp',
|
||||
files: [
|
||||
{
|
||||
id: 'some-id',
|
||||
type: AssetFileType.Sidecar,
|
||||
path: '/path/to/something',
|
||||
isEdited: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.webp' }).file({ type: AssetFileType.Sidecar }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
|
||||
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
});
|
||||
|
||||
it('should ignore all Duration tags for definitely static images', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.imageDng);
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.imageDng.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null }));
|
||||
@@ -1168,10 +1165,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should trim whitespace from description', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Description: '\t \v \f \n \r' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
description: '',
|
||||
@@ -1180,7 +1178,7 @@ describe(MetadataService.name, () => {
|
||||
);
|
||||
|
||||
mockReadTags({ ImageDescription: ' my\n description' });
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
description: 'my\n description',
|
||||
@@ -1190,10 +1188,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle a numeric description', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Description: 1000 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
description: '1000',
|
||||
@@ -1203,40 +1202,44 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip importing metadata when the feature is disabled', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } });
|
||||
mockReadTags(makeFaceTags({ Name: 'Person 1' }));
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.person.getDistinctNames).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags();
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.person.getDistinctNames).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip importing faces without name', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags());
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
mocks.person.createAll.mockResolvedValue([]);
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.person.createAll).not.toHaveBeenCalled();
|
||||
expect(mocks.person.refreshFaces).not.toHaveBeenCalled();
|
||||
expect(mocks.person.updateAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip importing faces with empty name', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: '' }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
mocks.person.createAll.mockResolvedValue([]);
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.person.createAll).not.toHaveBeenCalled();
|
||||
expect(mocks.person.refreshFaces).not.toHaveBeenCalled();
|
||||
expect(mocks.person.updateAll).not.toHaveBeenCalled();
|
||||
@@ -1414,10 +1417,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle invalid modify date', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ ModifyDate: '00:00:00.000' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modifyDate: expect.any(Date),
|
||||
@@ -1427,10 +1431,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle invalid rating value', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Rating: 6 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rating: null,
|
||||
@@ -1440,10 +1445,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle valid rating value', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Rating: 5 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rating: 5,
|
||||
@@ -1453,10 +1459,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle valid negative rating value', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Rating: -1 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rating: -1,
|
||||
@@ -1466,11 +1473,12 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle livePhotoCID not set', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ visibility: AssetVisibility.Hidden }),
|
||||
@@ -1579,10 +1587,11 @@ describe(MetadataService.name, () => {
|
||||
},
|
||||
{ exif: { AndroidMake: '1', AndroidModel: '2' }, expected: { make: '1', model: '2' } },
|
||||
])('should read camera make and model $exif -> $expected', async ({ exif, expected }) => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags(exif);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining(expected), {
|
||||
lockedPropertiesBehavior: 'skip',
|
||||
});
|
||||
@@ -1603,10 +1612,11 @@ describe(MetadataService.name, () => {
|
||||
{ exif: { LensID: ' Unknown 6-30mm' }, expected: null },
|
||||
{ exif: { LensID: '' }, expected: null },
|
||||
])('should read camera lens information $exif -> $expected', async ({ exif, expected }) => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags(exif);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
lensModel: expected,
|
||||
@@ -1616,10 +1626,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should properly set width/height for normal images', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
width: 1000,
|
||||
@@ -1629,10 +1640,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should properly swap asset width/height for rotated images', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
width: 2000,
|
||||
@@ -1642,14 +1654,11 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should not overwrite existing width/height if they already exist', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
});
|
||||
const asset = AssetFactory.create({ width: 1920, height: 1080 });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ ImageWidth: 1280, ImageHeight: 720 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
width: 1280,
|
||||
@@ -1685,7 +1694,7 @@ describe(MetadataService.name, () => {
|
||||
it('should do nothing if asset could not be found', async () => {
|
||||
mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(void 0);
|
||||
|
||||
await expect(sut.handleSidecarCheck({ id: assetStub.image.id })).resolves.toBeUndefined();
|
||||
await expect(sut.handleSidecarCheck({ id: 'non-existent' })).resolves.toBeUndefined();
|
||||
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { NotificationService } from 'src/services/notification.service';
|
||||
import { INotifyAlbumUpdateJob } from 'src/types';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { AssetFileFactory } from 'test/factories/asset-file.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { notificationStub } from 'test/fixtures/notification.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
@@ -392,8 +393,8 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
|
||||
it('should send invite email with album thumbnail and arbitrary extension', async () => {
|
||||
const assetFile = AssetFileFactory.create({ path: 'some-thumb.ext', type: AssetFileType.Thumbnail });
|
||||
const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId });
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build();
|
||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).asset(asset).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
@@ -407,7 +408,7 @@ describe(NotificationService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetFile]);
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([asset.files[0]]);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith(
|
||||
@@ -418,7 +419,7 @@ describe(NotificationService.name, () => {
|
||||
name: JobName.SendMail,
|
||||
data: expect.objectContaining({
|
||||
subject: expect.stringContaining('You have been added to a shared album'),
|
||||
imageAttachments: [{ filename: 'album-thumbnail.ext', path: expect.anything(), cid: expect.anything() }],
|
||||
imageAttachments: [{ filename: 'album-thumbnail.jpg', path: expect.anything(), cid: expect.anything() }],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum';
|
||||
import { AssetFileType, AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum';
|
||||
import { OcrService } from 'src/services/ocr.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@@ -14,7 +14,7 @@ describe(OcrService.name, () => {
|
||||
mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices);
|
||||
mocks.assetJob.getForOcr.mockResolvedValue({
|
||||
visibility: AssetVisibility.Timeline,
|
||||
previewFile: assetStub.image.files[1].path,
|
||||
previewFile: '/uploads/user-id/thumbs/path.jpg',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,20 +41,22 @@ describe(OcrService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue the assets without ocr', async () => {
|
||||
mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueOcr({ force: false });
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: assetStub.image.id } }]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: asset.id } }]);
|
||||
expect(mocks.assetJob.streamForOcrJob).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should queue all the assets', async () => {
|
||||
mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueOcr({ force: true });
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: assetStub.image.id } }]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: asset.id } }]);
|
||||
expect(mocks.assetJob.streamForOcrJob).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -70,15 +72,17 @@ describe(OcrService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip assets without a resize path', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForOcr.mockResolvedValue({ visibility: AssetVisibility.Timeline, previewFile: null });
|
||||
|
||||
expect(await sut.handleOcr({ id: assetStub.noResizePath.id })).toEqual(JobStatus.Failed);
|
||||
expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Failed);
|
||||
|
||||
expect(mocks.ocr.upsert).not.toHaveBeenCalled();
|
||||
expect(mocks.machineLearning.ocr).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save the returned objects', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.machineLearning.ocr.mockResolvedValue({
|
||||
box: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160],
|
||||
boxScore: [0.9, 0.8],
|
||||
@@ -86,7 +90,7 @@ describe(OcrService.name, () => {
|
||||
textScore: [0.95, 0.85],
|
||||
});
|
||||
|
||||
expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Success);
|
||||
expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Success);
|
||||
|
||||
expect(mocks.machineLearning.ocr).toHaveBeenCalledWith(
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
@@ -98,10 +102,10 @@ describe(OcrService.name, () => {
|
||||
}),
|
||||
);
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(
|
||||
assetStub.image.id,
|
||||
asset.id,
|
||||
[
|
||||
{
|
||||
assetId: assetStub.image.id,
|
||||
assetId: asset.id,
|
||||
boxScore: 0.9,
|
||||
text: 'One Two Three',
|
||||
textScore: 0.95,
|
||||
@@ -115,7 +119,7 @@ describe(OcrService.name, () => {
|
||||
y4: 80,
|
||||
},
|
||||
{
|
||||
assetId: assetStub.image.id,
|
||||
assetId: asset.id,
|
||||
boxScore: 0.8,
|
||||
text: 'Four Five',
|
||||
textScore: 0.85,
|
||||
@@ -134,6 +138,7 @@ describe(OcrService.name, () => {
|
||||
});
|
||||
|
||||
it('should apply config settings', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
machineLearning: {
|
||||
enabled: true,
|
||||
@@ -148,7 +153,7 @@ describe(OcrService.name, () => {
|
||||
});
|
||||
mockOcrResult();
|
||||
|
||||
expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Success);
|
||||
expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Success);
|
||||
|
||||
expect(mocks.machineLearning.ocr).toHaveBeenCalledWith(
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
@@ -159,16 +164,17 @@ describe(OcrService.name, () => {
|
||||
maxResolution: 1500,
|
||||
}),
|
||||
);
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, [], '');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, [], '');
|
||||
});
|
||||
|
||||
it('should skip invisible assets', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
mocks.assetJob.getForOcr.mockResolvedValue({
|
||||
visibility: AssetVisibility.Hidden,
|
||||
previewFile: assetStub.image.files[1].path,
|
||||
previewFile: asset.files[0].path,
|
||||
});
|
||||
|
||||
expect(await sut.handleOcr({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped);
|
||||
expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Skipped);
|
||||
|
||||
expect(mocks.machineLearning.ocr).not.toHaveBeenCalled();
|
||||
expect(mocks.ocr.upsert).not.toHaveBeenCalled();
|
||||
@@ -177,7 +183,7 @@ describe(OcrService.name, () => {
|
||||
it('should fail if asset could not be found', async () => {
|
||||
mocks.assetJob.getForOcr.mockResolvedValue(void 0);
|
||||
|
||||
expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Failed);
|
||||
expect(await sut.handleOcr({ id: 'non-existent' })).toEqual(JobStatus.Failed);
|
||||
|
||||
expect(mocks.machineLearning.ocr).not.toHaveBeenCalled();
|
||||
expect(mocks.ocr.upsert).not.toHaveBeenCalled();
|
||||
@@ -185,79 +191,84 @@ describe(OcrService.name, () => {
|
||||
|
||||
describe('search tokenization', () => {
|
||||
it('should generate bigrams for Chinese text', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult('機器學習');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 器學 學習');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '機器 器學 學習');
|
||||
});
|
||||
|
||||
it('should generate bigrams for Japanese text', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult('テスト');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'テス スト');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'テス スト');
|
||||
});
|
||||
|
||||
it('should generate bigrams for Korean text', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult('한국어');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '한국 국어');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '한국 국어');
|
||||
});
|
||||
|
||||
it('should pass through Latin text unchanged', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult('Hello World');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'Hello World');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'Hello World');
|
||||
});
|
||||
|
||||
it('should handle mixed CJK and Latin text', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult('機器學習Model');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 器學 學習 Model');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '機器 器學 學習 Model');
|
||||
});
|
||||
|
||||
it('should handle year followed by CJK', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult('2024年レポート');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(
|
||||
assetStub.image.id,
|
||||
expect.any(Array),
|
||||
'2024 年レ レポ ポー ート',
|
||||
);
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '2024 年レ レポ ポー ート');
|
||||
});
|
||||
|
||||
it('should join multiple OCR boxes', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult('機器', 'Learning');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 Learning');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '機器 Learning');
|
||||
});
|
||||
|
||||
it('should normalize whitespace', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult(' Hello World ');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'Hello World');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'Hello World');
|
||||
});
|
||||
|
||||
it('should keep single CJK characters', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mockOcrResult('A', '中', 'B');
|
||||
|
||||
await sut.handleOcr({ id: assetStub.image.id });
|
||||
await sut.handleOcr({ id: asset.id });
|
||||
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'A 中 B');
|
||||
expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'A 中 B');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { mapFaces, mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import { CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum';
|
||||
import { AssetFileType, CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum';
|
||||
import { DetectedFaces } from 'src/repositories/machine-learning.repository';
|
||||
import { FaceSearchResult } from 'src/repositories/search.repository';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
@@ -261,7 +261,7 @@ describe(PersonService.name, () => {
|
||||
it("should update a person's thumbnailPath", async () => {
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
mocks.person.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([faceStub.face1.assetId]));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
|
||||
await expect(
|
||||
@@ -331,7 +331,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
await expect(
|
||||
sut.reassignFaces(authStub.admin, personStub.noName.id, {
|
||||
data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }],
|
||||
data: [{ personId: personStub.withName.id, assetId: faceStub.face1.assetId }],
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
@@ -352,9 +352,10 @@ describe(PersonService.name, () => {
|
||||
|
||||
describe('getFacesById', () => {
|
||||
it('should get the bounding boxes for an asset', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([faceStub.face1.assetId]));
|
||||
const asset = AssetFactory.from({ id: faceStub.face1.assetId }).exif().build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.person.getFaces.mockResolvedValue([faceStub.primaryFace1]);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
await expect(sut.getFacesById(authStub.admin, { id: faceStub.face1.assetId })).resolves.toStrictEqual([
|
||||
mapFaces(faceStub.primaryFace1, authStub.admin),
|
||||
]);
|
||||
@@ -455,7 +456,8 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue missing assets', async () => {
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueDetectFaces({ force: false });
|
||||
|
||||
@@ -464,13 +466,14 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetDetectFaces,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should queue all assets', async () => {
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.withName]);
|
||||
|
||||
await sut.handleQueueDetectFaces({ force: true });
|
||||
@@ -483,13 +486,14 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetDetectFaces,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should refresh all assets', async () => {
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueDetectFaces({ force: undefined });
|
||||
|
||||
@@ -501,16 +505,17 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetDetectFaces,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.PersonCleanup });
|
||||
});
|
||||
|
||||
it('should delete existing people and faces if forced', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.person.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson]));
|
||||
mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image]));
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
|
||||
mocks.person.deleteFaces.mockResolvedValue();
|
||||
|
||||
@@ -520,7 +525,7 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetDetectFaces,
|
||||
data: { id: assetStub.image.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||
@@ -718,26 +723,28 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip when no resize path', async () => {
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.noResizePath, files: [] });
|
||||
await sut.handleDetectFaces({ id: assetStub.noResizePath.id });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
expect(mocks.machineLearning.detectFaces).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle no results', async () => {
|
||||
const start = Date.now();
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue({ imageHeight: 500, imageWidth: 400, faces: [] });
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] });
|
||||
await sut.handleDetectFaces({ id: assetStub.image.id });
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith(
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
asset.files[0].path,
|
||||
expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
|
||||
);
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
|
||||
expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith({
|
||||
assetId: assetStub.image.id,
|
||||
assetId: asset.id,
|
||||
facesRecognizedAt: expect.any(Date),
|
||||
});
|
||||
const facesRecognizedAt = mocks.asset.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date;
|
||||
@@ -745,14 +752,15 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should create a face with no person and queue recognition job', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock);
|
||||
mocks.search.searchFaces.mockResolvedValue([{ ...faceStub.face1, distance: 0.7 }]);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] });
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.person.refreshFaces.mockResolvedValue();
|
||||
|
||||
await sut.handleDetectFaces({ id: assetStub.image.id });
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]);
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith([{ ...face, assetId: asset.id }], [], [faceSearch]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.FacialRecognitionQueueAll, data: { force: false } },
|
||||
{ name: JobName.FacialRecognition, data: { id: faceId } },
|
||||
@@ -762,14 +770,11 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should delete an existing face not among the new detected faces', async () => {
|
||||
const asset = AssetFactory.from().face(faceStub.primaryFace1).file({ type: AssetFileType.Preview }).build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue({ faces: [], imageHeight: 500, imageWidth: 400 });
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
faces: [faceStub.primaryFace1],
|
||||
files: [assetStub.image.files[1]],
|
||||
});
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleDetectFaces({ id: assetStub.image.id });
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith([], [faceStub.primaryFace1.id], []);
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
@@ -778,17 +783,18 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should add new face and delete an existing face not among the new detected faces', async () => {
|
||||
const asset = AssetFactory.from().face(faceStub.primaryFace1).file({ type: AssetFileType.Preview }).build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
faces: [faceStub.primaryFace1],
|
||||
files: [assetStub.image.files[1]],
|
||||
});
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.person.refreshFaces.mockResolvedValue();
|
||||
|
||||
await sut.handleDetectFaces({ id: assetStub.image.id });
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [faceStub.primaryFace1.id], [faceSearch]);
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
||||
[{ ...face, assetId: asset.id }],
|
||||
[faceStub.primaryFace1.id],
|
||||
[faceSearch],
|
||||
);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.FacialRecognitionQueueAll, data: { force: false } },
|
||||
{ name: JobName.FacialRecognition, data: { id: faceId } },
|
||||
@@ -798,15 +804,12 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should add embedding to matching metadata face', async () => {
|
||||
const asset = AssetFactory.from().face(faceStub.fromExif1).file({ type: AssetFileType.Preview }).build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
faces: [faceStub.fromExif1],
|
||||
files: [assetStub.image.files[1]],
|
||||
});
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.person.refreshFaces.mockResolvedValue();
|
||||
|
||||
await sut.handleDetectFaces({ id: assetStub.image.id });
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
||||
[],
|
||||
@@ -819,16 +822,13 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should not add embedding to non-matching metadata face', async () => {
|
||||
const asset = AssetFactory.from().face(faceStub.fromExif2).file({ type: AssetFileType.Preview }).build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
faces: [faceStub.fromExif2],
|
||||
files: [assetStub.image.files[1]],
|
||||
});
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleDetectFaces({ id: assetStub.image.id });
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]);
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith([{ ...face, assetId: asset.id }], [], [faceSearch]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.FacialRecognitionQueueAll, data: { force: false } },
|
||||
{ name: JobName.FacialRecognition, data: { id: faceId } },
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||
import _ from 'lodash';
|
||||
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { SharedLinkFactory } from 'test/factories/shared-link.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
@@ -142,12 +142,13 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should create an individual shared link', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [assetStub.image.id],
|
||||
assetIds: [asset.id],
|
||||
showMetadata: true,
|
||||
allowDownload: true,
|
||||
allowUpload: true,
|
||||
@@ -155,7 +156,7 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
new Set([asset.id]),
|
||||
false,
|
||||
);
|
||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||
@@ -165,7 +166,7 @@ describe(SharedLinkService.name, () => {
|
||||
allowDownload: true,
|
||||
slug: null,
|
||||
allowUpload: true,
|
||||
assetIds: [assetStub.image.id],
|
||||
assetIds: [asset.id],
|
||||
description: null,
|
||||
expiresAt: null,
|
||||
showExif: true,
|
||||
@@ -174,12 +175,13 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should create a shared link with allowDownload set to false when showMetadata is false', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [assetStub.image.id],
|
||||
assetIds: [asset.id],
|
||||
showMetadata: false,
|
||||
allowDownload: true,
|
||||
allowUpload: true,
|
||||
@@ -187,7 +189,7 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
new Set([asset.id]),
|
||||
false,
|
||||
);
|
||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||
@@ -196,7 +198,7 @@ describe(SharedLinkService.name, () => {
|
||||
albumId: null,
|
||||
allowDownload: false,
|
||||
allowUpload: true,
|
||||
assetIds: [assetStub.image.id],
|
||||
assetIds: [asset.id],
|
||||
description: null,
|
||||
expiresAt: null,
|
||||
showExif: false,
|
||||
@@ -263,25 +265,28 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should add assets to a shared link', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3']));
|
||||
const asset = AssetFactory.create();
|
||||
const sharedLink = SharedLinkFactory.from().asset(asset).build();
|
||||
const newAsset = AssetFactory.create();
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.update.mockResolvedValue(sharedLink);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([newAsset.id]));
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2', 'asset-3'] }),
|
||||
sut.addAssets(authStub.admin, sharedLink.id, { assetIds: [asset.id, 'asset-2', newAsset.id] }),
|
||||
).resolves.toEqual([
|
||||
{ assetId: assetStub.image.id, success: false, error: AssetIdErrorReason.DUPLICATE },
|
||||
{ assetId: asset.id, success: false, error: AssetIdErrorReason.DUPLICATE },
|
||||
{ assetId: 'asset-2', success: false, error: AssetIdErrorReason.NO_PERMISSION },
|
||||
{ assetId: 'asset-3', success: true },
|
||||
{ assetId: newAsset.id, success: true },
|
||||
]);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
||||
...sharedLinkStub.individual,
|
||||
...sharedLink,
|
||||
slug: null,
|
||||
assetIds: ['asset-3'],
|
||||
assetIds: [newAsset.id],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -296,20 +301,22 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should remove assets from a shared link', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.sharedLinkAsset.remove.mockResolvedValue([assetStub.image.id]);
|
||||
const asset = AssetFactory.create();
|
||||
const sharedLink = SharedLinkFactory.from().asset(asset).build();
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.update.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLinkAsset.remove.mockResolvedValue([asset.id]);
|
||||
|
||||
await expect(
|
||||
sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }),
|
||||
sut.removeAssets(authStub.admin, sharedLink.id, { assetIds: [asset.id, 'asset-2'] }),
|
||||
).resolves.toEqual([
|
||||
{ assetId: assetStub.image.id, success: true },
|
||||
{ assetId: asset.id, success: true },
|
||||
{ assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND },
|
||||
]);
|
||||
|
||||
expect(mocks.sharedLinkAsset.remove).toHaveBeenCalledWith('link-1', [assetStub.image.id, 'asset-2']);
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] });
|
||||
expect(mocks.sharedLinkAsset.remove).toHaveBeenCalledWith(sharedLink.id, [asset.id, 'asset-2']);
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith(expect.objectContaining({ assets: [] }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -333,7 +340,7 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
|
||||
description: '1 shared photos & videos',
|
||||
imageUrl: `https://my.immich.app/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
|
||||
imageUrl: `https://my.immich.app/api/assets/${sharedLinkStub.individual.assets[0].id}/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
|
||||
title: 'Public Share',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { ImmichWorker, JobName, JobStatus } from 'src/enum';
|
||||
import { AssetFileType, AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum';
|
||||
import { SmartInfoService } from 'src/services/smart-info.service';
|
||||
import { getCLIPModelInfo } from 'src/utils/misc';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(SmartInfoService.name, () => {
|
||||
beforeEach(() => {
|
||||
({ sut, mocks } = newTestService(SmartInfoService));
|
||||
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||
mocks.asset.getByIds.mockResolvedValue([AssetFactory.create()]);
|
||||
mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices);
|
||||
});
|
||||
|
||||
@@ -155,25 +155,23 @@ describe(SmartInfoService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue the assets without clip embeddings', async () => {
|
||||
mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueEncodeClip({ force: false });
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.SmartSearch, data: { id: assetStub.image.id } },
|
||||
]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SmartSearch, data: { id: asset.id } }]);
|
||||
expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(false);
|
||||
expect(mocks.database.setDimensionSize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue all the assets', async () => {
|
||||
mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueEncodeClip({ force: true });
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.SmartSearch, data: { id: assetStub.image.id } },
|
||||
]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SmartSearch, data: { id: asset.id } }]);
|
||||
expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(true);
|
||||
expect(mocks.database.setDimensionSize).toHaveBeenCalledExactlyOnceWith(512);
|
||||
});
|
||||
@@ -190,34 +188,36 @@ describe(SmartInfoService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip assets without a resize path', async () => {
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.noResizePath, files: [] });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue(asset);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.Failed);
|
||||
expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Failed);
|
||||
|
||||
expect(mocks.search.upsert).not.toHaveBeenCalled();
|
||||
expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save the returned objects', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] });
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue(asset);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success);
|
||||
expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Success);
|
||||
|
||||
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
asset.files[0].path,
|
||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||
);
|
||||
expect(mocks.search.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
|
||||
expect(mocks.search.upsert).toHaveBeenCalledWith(asset.id, '[0.01, 0.02, 0.03]');
|
||||
});
|
||||
|
||||
it('should skip invisible assets', async () => {
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
files: [assetStub.image.files[1]],
|
||||
});
|
||||
const asset = AssetFactory.from({ visibility: AssetVisibility.Hidden })
|
||||
.file({ type: AssetFileType.Preview })
|
||||
.build();
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue(asset);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped);
|
||||
expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Skipped);
|
||||
|
||||
expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled();
|
||||
expect(mocks.search.upsert).not.toHaveBeenCalled();
|
||||
@@ -226,25 +226,26 @@ describe(SmartInfoService.name, () => {
|
||||
it('should fail if asset could not be found', async () => {
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue(void 0);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Failed);
|
||||
expect(await sut.handleEncodeClip({ id: 'non-existent' })).toEqual(JobStatus.Failed);
|
||||
|
||||
expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled();
|
||||
expect(mocks.search.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should wait for database', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
mocks.database.isBusy.mockReturnValue(true);
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] });
|
||||
mocks.assetJob.getForClipEncoding.mockResolvedValue(asset);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success);
|
||||
expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Success);
|
||||
|
||||
expect(mocks.database.wait).toHaveBeenCalledWith(512);
|
||||
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
asset.files[0].path,
|
||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||
);
|
||||
expect(mocks.search.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
|
||||
expect(mocks.search.upsert).toHaveBeenCalledWith(asset.id, '[0.01, 0.02, 0.03]');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { StackService } from 'src/services/stack.service';
|
||||
import { assetStub, stackStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { stackStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -19,38 +20,36 @@ describe(StackService.name, () => {
|
||||
|
||||
describe('search', () => {
|
||||
it('should search stacks', async () => {
|
||||
mocks.stack.search.mockResolvedValue([stackStub('stack-id', [assetStub.image])]);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.stack.search.mockResolvedValue([stackStub('stack-id', [asset])]);
|
||||
|
||||
await sut.search(authStub.admin, { primaryAssetId: assetStub.image.id });
|
||||
await sut.search(authStub.admin, { primaryAssetId: asset.id });
|
||||
expect(mocks.stack.search).toHaveBeenCalledWith({
|
||||
ownerId: authStub.admin.user.id,
|
||||
primaryAssetId: assetStub.image.id,
|
||||
primaryAssetId: asset.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should require asset.update permissions', async () => {
|
||||
await expect(
|
||||
sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
await expect(sut.create(authStub.admin, { assetIds: [primaryAsset.id, asset.id] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.stack.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a stack', async () => {
|
||||
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({
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([primaryAsset.id, asset.id]));
|
||||
mocks.stack.create.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
await expect(sut.create(authStub.admin, { assetIds: [primaryAsset.id, asset.id] })).resolves.toEqual({
|
||||
id: 'stack-id',
|
||||
primaryAssetId: assetStub.image.id,
|
||||
assets: [
|
||||
expect.objectContaining({ id: assetStub.image.id }),
|
||||
expect.objectContaining({ id: assetStub.image1.id }),
|
||||
],
|
||||
primaryAssetId: primaryAsset.id,
|
||||
assets: [expect.objectContaining({ id: primaryAsset.id }), expect.objectContaining({ id: asset.id })],
|
||||
});
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackCreate', {
|
||||
@@ -79,16 +78,14 @@ describe(StackService.name, () => {
|
||||
});
|
||||
|
||||
it('should get stack', async () => {
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
|
||||
await expect(sut.get(authStub.admin, 'stack-id')).resolves.toEqual({
|
||||
id: 'stack-id',
|
||||
primaryAssetId: assetStub.image.id,
|
||||
assets: [
|
||||
expect.objectContaining({ id: assetStub.image.id }),
|
||||
expect.objectContaining({ id: assetStub.image1.id }),
|
||||
],
|
||||
primaryAssetId: primaryAsset.id,
|
||||
assets: [expect.objectContaining({ id: primaryAsset.id }), expect.objectContaining({ id: asset.id })],
|
||||
});
|
||||
expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
@@ -115,8 +112,9 @@ describe(StackService.name, () => {
|
||||
});
|
||||
|
||||
it('should fail if the provided primary asset id is not in the stack', async () => {
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1]));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
|
||||
await expect(sut.update(authStub.admin, 'stack-id', { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
@@ -128,16 +126,17 @@ describe(StackService.name, () => {
|
||||
});
|
||||
|
||||
it('should update stack', async () => {
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
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]));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
mocks.stack.update.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
|
||||
await sut.update(authStub.admin, 'stack-id', { primaryAssetId: assetStub.image1.id });
|
||||
await sut.update(authStub.admin, 'stack-id', { primaryAssetId: asset.id });
|
||||
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.update).toHaveBeenCalledWith('stack-id', {
|
||||
id: 'stack-id',
|
||||
primaryAssetId: assetStub.image1.id,
|
||||
primaryAssetId: asset.id,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', {
|
||||
stackId: 'stack-id',
|
||||
@@ -214,24 +213,26 @@ describe(StackService.name, () => {
|
||||
});
|
||||
|
||||
it('should fail if the assetId is the primaryAssetId', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: assetStub.image.id });
|
||||
mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: asset.id });
|
||||
|
||||
await expect(
|
||||
sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.image.id }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: asset.id })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update the asset to nullify it's stack-id", async () => {
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: assetStub.image.id });
|
||||
mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: primaryAsset.id });
|
||||
|
||||
await sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.image1.id });
|
||||
await sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: asset.id });
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.image1.id, stackId: null });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, stackId: null });
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
|
||||
@@ -478,9 +478,9 @@ describe(StorageTemplateService.name, () => {
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: assetStub.image.id,
|
||||
entityId: asset.id,
|
||||
pathType: AssetPathType.Original,
|
||||
oldPath: assetStub.image.originalPath,
|
||||
oldPath: asset.originalPath,
|
||||
newPath,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
@@ -60,10 +61,9 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should return a response requiring a full sync when there are too many changes', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue(
|
||||
Array.from<typeof assetStub.image>({ length: 10_000 }).fill(assetStub.image),
|
||||
);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue(Array.from<typeof asset>({ length: 10_000 }).fill(asset));
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
|
||||
@@ -72,14 +72,15 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should return a response with changes and deletions', async () => {
|
||||
const asset = AssetFactory.create({ ownerId: authStub.user1.user.id });
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue([assetStub.image1]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue([asset]);
|
||||
mocks.audit.getAfter.mockResolvedValue([assetStub.external.id]);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({
|
||||
needsFullSync: false,
|
||||
upserted: [mapAsset(assetStub.image1, mapAssetOpts)],
|
||||
upserted: [mapAsset(asset, mapAssetOpts)],
|
||||
deleted: [assetStub.external.id],
|
||||
});
|
||||
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { ViewService } from 'src/services/view.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@@ -32,8 +32,8 @@ describe(ViewService.name, () => {
|
||||
it('should return assets by original path', async () => {
|
||||
const path = '/asset';
|
||||
|
||||
const asset1 = { ...assetStub.image, originalPath: '/asset/path1' };
|
||||
const asset2 = { ...assetStub.image, originalPath: '/asset/path2' };
|
||||
const asset1 = AssetFactory.create({ originalPath: '/asset/path1' });
|
||||
const asset2 = AssetFactory.create({ originalPath: '/asset/path2' });
|
||||
|
||||
const mockAssets = [asset1, asset2];
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export class AssetFactory {
|
||||
#assetExif?: AssetExifFactory;
|
||||
#files: AssetFileFactory[] = [];
|
||||
#edits: AssetEditFactory[] = [];
|
||||
#faces: Selectable<AssetFaceTable>[] = [];
|
||||
|
||||
private constructor(private readonly value: Selectable<AssetTable>) {
|
||||
value.ownerId ??= newUuid();
|
||||
@@ -82,6 +83,11 @@ export class AssetFactory {
|
||||
return this;
|
||||
}
|
||||
|
||||
face(dto: Selectable<AssetFaceTable>) {
|
||||
this.#faces.push(dto);
|
||||
return this;
|
||||
}
|
||||
|
||||
file(dto: AssetFileLike = {}, builder?: FactoryBuilder<AssetFileFactory>) {
|
||||
this.#files.push(build(AssetFileFactory.from(dto).asset(this.value), builder));
|
||||
return this;
|
||||
@@ -120,7 +126,8 @@ export class AssetFactory {
|
||||
exifInfo: exif as NonNullable<typeof exif>,
|
||||
files: this.#files.map((file) => file.build()),
|
||||
edits: this.#edits.map((edit) => edit.build()),
|
||||
faces: [] as Selectable<AssetFaceTable>[],
|
||||
faces: this.#faces,
|
||||
stack: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ import { Selectable } from 'kysely';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { build } from 'test/factories/builder.factory';
|
||||
import { AlbumLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types';
|
||||
import { AlbumLike, AssetLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { factory, newDate, newUuid } from 'test/small.factory';
|
||||
|
||||
export class SharedLinkFactory {
|
||||
#owner: UserFactory;
|
||||
#album?: AlbumFactory;
|
||||
#assets: AssetFactory[] = [];
|
||||
|
||||
private constructor(private readonly value: Selectable<SharedLinkTable>) {
|
||||
value.userId ??= newUuid();
|
||||
@@ -52,12 +54,18 @@ export class SharedLinkFactory {
|
||||
return this;
|
||||
}
|
||||
|
||||
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
|
||||
const asset = build(AssetFactory.from(dto), builder);
|
||||
this.#assets.push(asset);
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return {
|
||||
...this.value,
|
||||
owner: this.#owner.build(),
|
||||
album: this.#album?.build(),
|
||||
assets: [],
|
||||
album: this.#album?.build() ?? null,
|
||||
assets: this.#assets.map((asset) => asset.build()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
129
server/test/fixtures/asset.stub.ts
vendored
129
server/test/fixtures/asset.stub.ts
vendored
@@ -55,45 +55,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
...asset,
|
||||
}),
|
||||
noResizePath: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
originalFileName: 'IMG_123.jpg',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
owner: userStub.user1,
|
||||
ownerId: 'user-id',
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/data/library/IMG_123.jpg',
|
||||
files: [thumbnailFile],
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: true,
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
faces: [],
|
||||
exifInfo: {} as Exif,
|
||||
deletedAt: null,
|
||||
isExternal: false,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
updateId: '42',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
primaryImage: Object.freeze({
|
||||
id: 'primary-asset-id',
|
||||
@@ -144,53 +105,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
image: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
deviceAssetId: 'device-asset-id',
|
||||
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
owner: userStub.user1,
|
||||
ownerId: 'user-id',
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/original/path.jpg',
|
||||
files,
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
|
||||
isFavorite: true,
|
||||
duration: null,
|
||||
isExternal: false,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
updateId: 'foo',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.jpg',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
exifImageHeight: 3840,
|
||||
exifImageWidth: 2160,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
stack: null,
|
||||
orientation: '',
|
||||
projectionType: null,
|
||||
height: null,
|
||||
width: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
trashed: Object.freeze({
|
||||
id: 'asset-id',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -365,49 +279,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
image1: Object.freeze({
|
||||
id: 'asset-id-1',
|
||||
status: AssetStatus.Active,
|
||||
deviceAssetId: 'device-asset-id',
|
||||
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
owner: userStub.user1,
|
||||
ownerId: 'user-id',
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/original/path.ext',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
files,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
deletedAt: null,
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: true,
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
isExternal: false,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.ext',
|
||||
faces: [],
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
updateId: '42',
|
||||
stackId: null,
|
||||
libraryId: null,
|
||||
stack: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
video: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
|
||||
34
server/test/fixtures/face.stub.ts
vendored
34
server/test/fixtures/face.stub.ts
vendored
@@ -1,13 +1,13 @@
|
||||
import { SourceType } from 'src/enum';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
|
||||
export const faceStub = {
|
||||
face1: Object.freeze({
|
||||
id: 'assetFaceId1',
|
||||
assetId: assetStub.image.id,
|
||||
assetId: 'asset-id',
|
||||
asset: {
|
||||
...assetStub.image,
|
||||
...AssetFactory.create({ id: 'asset-id' }),
|
||||
libraryId: null,
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
stackId: null,
|
||||
@@ -29,8 +29,8 @@ export const faceStub = {
|
||||
}),
|
||||
primaryFace1: Object.freeze({
|
||||
id: 'assetFaceId2',
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.primaryPerson.id,
|
||||
person: personStub.primaryPerson,
|
||||
boundingBoxX1: 0,
|
||||
@@ -48,8 +48,8 @@ export const faceStub = {
|
||||
}),
|
||||
mergeFace1: Object.freeze({
|
||||
id: 'assetFaceId3',
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.mergePerson.id,
|
||||
person: personStub.mergePerson,
|
||||
boundingBoxX1: 0,
|
||||
@@ -67,8 +67,8 @@ export const faceStub = {
|
||||
}),
|
||||
noPerson1: Object.freeze({
|
||||
id: 'assetFaceId8',
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: null,
|
||||
person: null,
|
||||
boundingBoxX1: 0,
|
||||
@@ -86,8 +86,8 @@ export const faceStub = {
|
||||
}),
|
||||
noPerson2: Object.freeze({
|
||||
id: 'assetFaceId9',
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: null,
|
||||
person: null,
|
||||
boundingBoxX1: 0,
|
||||
@@ -105,8 +105,8 @@ export const faceStub = {
|
||||
}),
|
||||
fromExif1: Object.freeze({
|
||||
id: 'assetFaceId9',
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.randomPerson.id,
|
||||
person: personStub.randomPerson,
|
||||
boundingBoxX1: 100,
|
||||
@@ -123,8 +123,8 @@ export const faceStub = {
|
||||
}),
|
||||
fromExif2: Object.freeze({
|
||||
id: 'assetFaceId9',
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.randomPerson.id,
|
||||
person: personStub.randomPerson,
|
||||
boundingBoxX1: 0,
|
||||
@@ -141,8 +141,8 @@ export const faceStub = {
|
||||
}),
|
||||
withBirthDate: Object.freeze({
|
||||
id: 'assetFaceId10',
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.withBirthDate.id,
|
||||
person: personStub.withBirthDate,
|
||||
boundingBoxX1: 0,
|
||||
|
||||
4
server/test/fixtures/shared-link.stub.ts
vendored
4
server/test/fixtures/shared-link.stub.ts
vendored
@@ -2,7 +2,7 @@ import { UserAdmin } from 'src/database';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
|
||||
import { AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
|
||||
@@ -31,7 +31,7 @@ export const sharedLinkStub = {
|
||||
albumId: null,
|
||||
album: null,
|
||||
description: null,
|
||||
assets: [assetStub.image],
|
||||
assets: [AssetFactory.create()],
|
||||
password: 'password',
|
||||
slug: null,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user