mirror of
https://github.com/immich-app/immich.git
synced 2026-02-06 19:13:48 -05:00
refactor: tests (#25987)
This commit is contained in:
parent
b3820c259e
commit
e3e243fa2b
@ -1,15 +1,18 @@
|
||||
import { mapAlbum } from 'src/dtos/album.dto';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
|
||||
describe('mapAlbum', () => {
|
||||
it('should set start and end dates', () => {
|
||||
const dto = mapAlbum(albumStub.twoAssets, false);
|
||||
expect(dto.startDate).toEqual(new Date('2020-12-31T23:59:00.000Z'));
|
||||
expect(dto.endDate).toEqual(new Date('2025-01-01T01:02:03.456Z'));
|
||||
const startDate = new Date('2023-02-22T05:06:29.716Z');
|
||||
const endDate = new Date('2025-01-01T01:02:03.456Z');
|
||||
const album = AlbumFactory.from().asset({ localDateTime: endDate }).asset({ localDateTime: startDate }).build();
|
||||
const dto = mapAlbum(album, false);
|
||||
expect(dto.startDate).toEqual(startDate);
|
||||
expect(dto.endDate).toEqual(endDate);
|
||||
});
|
||||
|
||||
it('should not set start and end dates for empty assets', () => {
|
||||
const dto = mapAlbum(albumStub.empty, false);
|
||||
const dto = mapAlbum(AlbumFactory.create(), false);
|
||||
expect(dto.startDate).toBeUndefined();
|
||||
expect(dto.endDate).toBeUndefined();
|
||||
});
|
||||
|
||||
@ -6,9 +6,10 @@ import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { 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';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -45,35 +46,33 @@ describe(AssetService.name, () => {
|
||||
|
||||
describe('getStatistics', () => {
|
||||
it('should get the statistics for a user, excluding archived assets', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Timeline })).resolves.toEqual(
|
||||
statResponse,
|
||||
);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
await expect(sut.getStatistics(auth, { visibility: AssetVisibility.Timeline })).resolves.toEqual(statResponse);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, { visibility: AssetVisibility.Timeline });
|
||||
});
|
||||
|
||||
it('should get the statistics for a user for archived assets', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Archive })).resolves.toEqual(
|
||||
statResponse,
|
||||
);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
|
||||
await expect(sut.getStatistics(auth, { visibility: AssetVisibility.Archive })).resolves.toEqual(statResponse);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, {
|
||||
visibility: AssetVisibility.Archive,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the statistics for a user for favorite assets', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isFavorite: true });
|
||||
await expect(sut.getStatistics(auth, { isFavorite: true })).resolves.toEqual(statResponse);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, { isFavorite: true });
|
||||
});
|
||||
|
||||
it('should get the statistics for a user for all assets', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||
await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {});
|
||||
await expect(sut.getStatistics(auth, {})).resolves.toEqual(statResponse);
|
||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, {});
|
||||
});
|
||||
});
|
||||
|
||||
@ -249,10 +248,11 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should fail linking a live video if the motion part could not be found', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
||||
sut.update(auth, assetStub.livePhotoStillAsset.id, {
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
@ -267,11 +267,12 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking a live video if the motion part is not a video', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
|
||||
@ -291,16 +292,17 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking a live video if the motion part has a different owner', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
||||
sut.update(auth, assetStub.livePhotoStillAsset.id, {
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
@ -315,52 +317,41 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should link a live video', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
ownerId: authStub.admin.user.id,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
|
||||
mocks.asset.update.mockResolvedValue(assetStub.image);
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Timeline });
|
||||
const stillAsset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([stillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(stillAsset);
|
||||
mocks.asset.update.mockResolvedValue(stillAsset);
|
||||
const auth = AuthFactory.from(motionAsset.owner).build();
|
||||
|
||||
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
await sut.update(auth, stillAsset.id, { livePhotoVideoId: motionAsset.id });
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, visibility: AssetVisibility.Hidden });
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { assetId: motionAsset.id, userId: auth.user.id });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, livePhotoVideoId: motionAsset.id });
|
||||
});
|
||||
|
||||
it('should throw an error if asset could not be found after update', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
await expect(sut.update(authStub.admin, 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf(
|
||||
await expect(sut.update(AuthFactory.create(), 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should unlink a live video', async () => {
|
||||
const auth = AuthFactory.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);
|
||||
|
||||
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
|
||||
await sut.update(auth, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
@ -372,7 +363,7 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
@ -392,17 +383,15 @@ describe(AssetService.name, () => {
|
||||
|
||||
describe('updateAll', () => {
|
||||
it('should require asset write access for all ids', async () => {
|
||||
await expect(
|
||||
sut.updateAll(authStub.admin, {
|
||||
ids: ['asset-1'],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
const auth = AuthFactory.create();
|
||||
await expect(sut.updateAll(auth, { ids: ['asset-1'] })).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should update all assets', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
|
||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.Archive });
|
||||
await sut.updateAll(auth, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.Archive });
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], {
|
||||
visibility: AssetVisibility.Archive,
|
||||
@ -410,9 +399,10 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should not update Assets table if no relevant fields are provided', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
|
||||
await sut.updateAll(authStub.admin, {
|
||||
await sut.updateAll(auth, {
|
||||
ids: ['asset-1'],
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
|
||||
@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
|
||||
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';
|
||||
@ -60,22 +61,22 @@ describe(DownloadService.name, () => {
|
||||
stream: new Readable(),
|
||||
};
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
const asset1 = AssetFactory.create();
|
||||
const asset2 = AssetFactory.create();
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
|
||||
mocks.storage.realpath.mockRejectedValue(new Error('Could not read file'));
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{ ...assetStub.noResizePath, id: 'asset-1' },
|
||||
{ ...assetStub.noWebpPath, id: 'asset-2' },
|
||||
]);
|
||||
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,
|
||||
});
|
||||
|
||||
expect(mocks.logger.warn).toHaveBeenCalledTimes(2);
|
||||
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg');
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_456.jpg', 'IMG_456.jpg');
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset1.originalPath, asset1.originalFileName);
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, asset2.originalPath, asset2.originalFileName);
|
||||
});
|
||||
|
||||
it('should download an archive', async () => {
|
||||
@ -85,20 +86,20 @@ describe(DownloadService.name, () => {
|
||||
stream: new Readable(),
|
||||
};
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{ ...assetStub.noResizePath, id: 'asset-1' },
|
||||
{ ...assetStub.noWebpPath, id: 'asset-2' },
|
||||
]);
|
||||
const asset1 = AssetFactory.create();
|
||||
const asset2 = AssetFactory.create();
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg');
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_456.jpg', 'IMG_456.jpg');
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset1.originalPath, asset1.originalFileName);
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, asset2.originalPath, asset2.originalFileName);
|
||||
});
|
||||
|
||||
it('should handle duplicate file names', async () => {
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from 'src/enum';
|
||||
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 { faceStub } from 'test/fixtures/face.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
@ -139,33 +140,30 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should queue all assets with missing webp path', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noWebpPath]));
|
||||
it('should queue all assets with missing preview', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false });
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetGenerateThumbnails,
|
||||
data: { id: assetStub.image.id },
|
||||
},
|
||||
{ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } },
|
||||
]);
|
||||
|
||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should queue all assets with missing thumbhash', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noThumbhash]));
|
||||
const asset = AssetFactory.from({ thumbhash: null })
|
||||
.files([AssetFileType.Thumbnail, AssetFileType.Preview])
|
||||
.build();
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false });
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetGenerateThumbnails,
|
||||
data: { id: assetStub.image.id },
|
||||
},
|
||||
{ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } },
|
||||
]);
|
||||
|
||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
@ -1052,12 +1050,19 @@ describe(MediaService.name, () => {
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
||||
const asset = AssetFactory.from({ originalFileName: 'image.hif' })
|
||||
.exif({
|
||||
fileSizeInByte: 5000,
|
||||
profileDescription: 'Adobe RGB',
|
||||
bitsPerSample: 14,
|
||||
})
|
||||
.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(assetStub.imageHif.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
processInvalidImages: false,
|
||||
});
|
||||
@ -1107,12 +1112,19 @@ describe(MediaService.name, () => {
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.media.copyTagGroup.mockResolvedValue(true);
|
||||
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.panoramaTif);
|
||||
const asset = AssetFactory.from({ originalFileName: 'panorama.tif' })
|
||||
.exif({
|
||||
fileSizeInByte: 5000,
|
||||
projectionType: 'EQUIRECTANGULAR',
|
||||
})
|
||||
.build();
|
||||
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.panoramaTif.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.Srgb,
|
||||
orientation: undefined,
|
||||
processInvalidImages: false,
|
||||
@ -1135,11 +1147,7 @@ describe(MediaService.name, () => {
|
||||
);
|
||||
|
||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledWith(
|
||||
'XMP-GPano',
|
||||
assetStub.panoramaTif.originalPath,
|
||||
expect.any(String),
|
||||
);
|
||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledWith('XMP-GPano', asset.originalPath, expect.any(String));
|
||||
});
|
||||
|
||||
it('should respect encoding options when generating full-size preview', async () => {
|
||||
@ -1149,12 +1157,19 @@ describe(MediaService.name, () => {
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
||||
const asset = AssetFactory.from({ originalFileName: 'image.hif' })
|
||||
.exif({
|
||||
fileSizeInByte: 5000,
|
||||
profileDescription: 'Adobe RGB',
|
||||
bitsPerSample: 14,
|
||||
})
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageHif.originalPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
processInvalidImages: false,
|
||||
});
|
||||
@ -1181,9 +1196,16 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
||||
const asset = AssetFactory.from({ originalFileName: 'image.hif' })
|
||||
.exif({
|
||||
fileSizeInByte: 5000,
|
||||
profileDescription: 'Adobe RGB',
|
||||
bitsPerSample: 14,
|
||||
})
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
@ -1263,30 +1285,25 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should clean up edited files if an asset has no edits', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||
...assetStub.withoutEdits,
|
||||
});
|
||||
const asset = AssetFactory.from({ thumbhash: factory.buffer() })
|
||||
.exif()
|
||||
.files([
|
||||
{ type: AssetFileType.Preview, path: 'edited1.jpg', isEdited: true },
|
||||
{ type: AssetFileType.Thumbnail, path: 'edited2.jpg', isEdited: true },
|
||||
{ type: AssetFileType.FullSize, path: 'edited3.jpg', isEdited: true },
|
||||
])
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
|
||||
const status = await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
||||
|
||||
const status = await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.FileDelete,
|
||||
data: {
|
||||
files: expect.arrayContaining([
|
||||
'/uploads/user-id/fullsize/path_edited.jpg',
|
||||
'/uploads/user-id/preview/path_edited.jpg',
|
||||
'/uploads/user-id/thumbnail/path_edited.jpg',
|
||||
]),
|
||||
files: expect.arrayContaining(['edited1.jpg', 'edited2.jpg', 'edited3.jpg']),
|
||||
},
|
||||
});
|
||||
|
||||
expect(mocks.asset.deleteFiles).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ path: '/uploads/user-id/preview/path_edited.jpg' }),
|
||||
expect.objectContaining({ path: '/uploads/user-id/thumbnail/path_edited.jpg' }),
|
||||
expect.objectContaining({ path: '/uploads/user-id/fullsize/path_edited.jpg' }),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(status).toBe(JobStatus.Success);
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
||||
@ -1320,11 +1337,9 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate the original thumbhash if no edits exist', async () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||
...assetStub.withoutEdits,
|
||||
});
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.media.generateThumbhash.mockResolvedValue(factory.buffer());
|
||||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id, source: 'upload' });
|
||||
|
||||
@ -1335,18 +1350,14 @@ describe(MediaService.name, () => {
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||
...assetStub.withCropEdit,
|
||||
});
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
const thumbhashBuffer = factory.buffer();
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
thumbhash: thumbhashBuffer,
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ thumbhash: thumbhashBuffer }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -185,7 +185,7 @@ export class MediaService extends BaseService {
|
||||
|
||||
const generated = await this.generateEditedThumbnails(asset, config);
|
||||
await this.syncFiles(
|
||||
asset.files.filter((asset) => asset.isEdited),
|
||||
asset.files.filter((file) => file.isEdited),
|
||||
generated?.files ?? [],
|
||||
);
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
} from 'src/enum';
|
||||
import { ImmichTags } from 'src/repositories/metadata.repository';
|
||||
import { firstDateTime, MetadataService } from 'src/services/metadata.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
@ -24,13 +25,6 @@ import { tagStub } from 'test/fixtures/tag.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const removeNonSidecarFiles = (asset: any) => {
|
||||
return {
|
||||
...asset,
|
||||
files: asset.files.filter((file: any) => file.type === AssetFileType.Sidecar),
|
||||
};
|
||||
};
|
||||
|
||||
const forSidecarJob = (
|
||||
asset: {
|
||||
id?: string;
|
||||
@ -182,17 +176,18 @@ describe(MetadataService.name, () => {
|
||||
it('should handle a date in a sidecar file', async () => {
|
||||
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
||||
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.sidecar));
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.sidecar.id);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }), {
|
||||
lockedPropertiesBehavior: 'skip',
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: assetStub.image.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
fileCreatedAt: sidecarDate,
|
||||
localDateTime: sidecarDate,
|
||||
@ -203,7 +198,8 @@ describe(MetadataService.name, () => {
|
||||
it('should take the file modification date when missing exif and earlier than creation date', async () => {
|
||||
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: fileModifiedAt,
|
||||
@ -212,14 +208,14 @@ describe(MetadataService.name, () => {
|
||||
} as Stats);
|
||||
mockReadTags();
|
||||
|
||||
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({ dateTimeOriginal: fileModifiedAt }),
|
||||
{ lockedPropertiesBehavior: 'skip' },
|
||||
);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
fileCreatedAt: fileModifiedAt,
|
||||
fileModifiedAt,
|
||||
@ -232,7 +228,8 @@ describe(MetadataService.name, () => {
|
||||
it('should take the file creation date when missing exif and earlier than modification date', async () => {
|
||||
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: fileModifiedAt,
|
||||
@ -241,14 +238,14 @@ describe(MetadataService.name, () => {
|
||||
} as Stats);
|
||||
mockReadTags();
|
||||
|
||||
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({ dateTimeOriginal: fileCreatedAt }),
|
||||
{ lockedPropertiesBehavior: 'skip' },
|
||||
);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
fileCreatedAt,
|
||||
fileModifiedAt,
|
||||
@ -260,10 +257,11 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should determine dateTimeOriginal regardless of the server time zone', async () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.sidecar));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dateTimeOriginal: new Date('2022-01-01T00:00:00.000Z'),
|
||||
@ -279,16 +277,15 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle lists of numbers', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: assetStub.image.fileModifiedAt,
|
||||
mtimeMs: assetStub.image.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: assetStub.image.fileCreatedAt.valueOf(),
|
||||
mtime: asset.fileModifiedAt,
|
||||
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||
} as Stats);
|
||||
mockReadTags({
|
||||
ISO: [160],
|
||||
});
|
||||
mockReadTags({ ISO: [160] });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||
@ -296,11 +293,11 @@ describe(MetadataService.name, () => {
|
||||
lockedPropertiesBehavior: 'skip',
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.image.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
fileCreatedAt: assetStub.image.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.image.fileCreatedAt,
|
||||
localDateTime: assetStub.image.fileCreatedAt,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileCreatedAt,
|
||||
localDateTime: asset.fileCreatedAt,
|
||||
width: null,
|
||||
height: null,
|
||||
});
|
||||
@ -308,77 +305,77 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should not delete latituide and longitude without reverse geocode', async () => {
|
||||
// regression test for issue 17511
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.withLocation);
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: assetStub.withLocation.fileModifiedAt,
|
||||
mtimeMs: assetStub.withLocation.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: assetStub.withLocation.fileCreatedAt.valueOf(),
|
||||
mtime: asset.fileModifiedAt,
|
||||
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||
} as Stats);
|
||||
mockReadTags({
|
||||
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
||||
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
||||
GPSLatitude: asset.exifInfo.latitude!,
|
||||
GPSLongitude: asset.exifInfo.longitude!,
|
||||
});
|
||||
|
||||
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({ city: null, state: null, country: null }),
|
||||
{ lockedPropertiesBehavior: 'skip' },
|
||||
);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.withLocation.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
fileCreatedAt: assetStub.withLocation.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.withLocation.fileModifiedAt,
|
||||
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
localDateTime: asset.localDateTime,
|
||||
width: null,
|
||||
height: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply reverse geocoding', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.withLocation));
|
||||
const asset = AssetFactory.from().exif({ latitude: 10, longitude: 20 }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
||||
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: assetStub.withLocation.fileModifiedAt,
|
||||
mtimeMs: assetStub.withLocation.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: assetStub.withLocation.fileCreatedAt.valueOf(),
|
||||
mtime: asset.fileModifiedAt,
|
||||
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||
} as Stats);
|
||||
mockReadTags({
|
||||
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
||||
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
||||
});
|
||||
mockReadTags({ GPSLatitude: 10, GPSLongitude: 20 });
|
||||
|
||||
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({ city: 'City', state: 'State', country: 'Country' }),
|
||||
{ lockedPropertiesBehavior: 'skip' },
|
||||
);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.withLocation.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
fileCreatedAt: assetStub.withLocation.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.withLocation.fileModifiedAt,
|
||||
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
localDateTime: asset.localDateTime,
|
||||
width: null,
|
||||
height: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should discard latitude and longitude on null island', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.withLocation));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({
|
||||
GPSLatitude: 0,
|
||||
GPSLongitude: 0,
|
||||
});
|
||||
|
||||
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({ latitude: null, longitude: null }),
|
||||
{ lockedPropertiesBehavior: 'skip' },
|
||||
@ -386,19 +383,25 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should extract tags from TagsList', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ TagsList: ['Parent'] });
|
||||
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: 'user-id', value: 'Parent', parent: undefined });
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||
});
|
||||
|
||||
it('should extract hierarchy from TagsList', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent/Child'] }) });
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent/Child'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ TagsList: ['Parent/Child'] });
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
||||
@ -406,135 +409,147 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.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 extract tags from Keywords as a string', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ Keywords: 'Parent' });
|
||||
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: 'user-id', value: 'Parent', parent: undefined });
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||
});
|
||||
|
||||
it('should extract tags from Keywords as a list', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ Keywords: ['Parent'] });
|
||||
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: 'user-id', value: 'Parent', parent: undefined });
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||
});
|
||||
|
||||
it('should extract tags from Keywords as a list with a number', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent', '2024'] }),
|
||||
});
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent', '2024'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ Keywords: ['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: 'user-id', value: 'Parent', parent: undefined });
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined });
|
||||
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 hierarchal tags from Keywords', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent/Child'] }) });
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent/Child'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ Keywords: '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 ignore Keywords when TagsList is present', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent/Child', 'Child'] }),
|
||||
});
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent/Child', 'Child'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ Keywords: 'Child', 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 extract hierarchy from HierarchicalSubject', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent/Child', 'TagA'] }),
|
||||
});
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent/Child', 'TagA'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
||||
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: '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',
|
||||
});
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined });
|
||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(3, {
|
||||
userId: asset.ownerId,
|
||||
value: 'TagA',
|
||||
parent: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
||||
mocks.asset.getById.mockResolvedValue({
|
||||
...factory.asset(),
|
||||
exifInfo: factory.exif({ tags: ['Parent', '2024'] }),
|
||||
});
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ tags: ['Parent', '2024'] })
|
||||
.build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
|
||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined });
|
||||
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 () => {
|
||||
@ -1646,31 +1661,23 @@ describe(MetadataService.name, () => {
|
||||
|
||||
describe('handleQueueSidecar', () => {
|
||||
it('should queue assets with sidecar files', async () => {
|
||||
mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueSidecar({ force: true });
|
||||
expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(true);
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.SidecarCheck,
|
||||
data: { id: assetStub.sidecar.id },
|
||||
},
|
||||
]);
|
||||
expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(true);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SidecarCheck, data: { id: asset.id } }]);
|
||||
});
|
||||
|
||||
it('should queue assets without sidecar files', async () => {
|
||||
mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([assetStub.image]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([asset]));
|
||||
|
||||
await sut.handleQueueSidecar({ force: false });
|
||||
|
||||
expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(false);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.SidecarCheck,
|
||||
data: { id: assetStub.image.id },
|
||||
},
|
||||
]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SidecarCheck, data: { id: asset.id } }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import { StackService } from 'src/services/stack.service';
|
||||
import { assetStub, 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';
|
||||
|
||||
describe(StackService.name, () => {
|
||||
@ -204,9 +205,9 @@ describe(StackService.name, () => {
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getForAssetRemoval.mockResolvedValue({ id: null, primaryAssetId: null });
|
||||
|
||||
await expect(
|
||||
sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.imageFrom2015.id }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: newUuid() })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
|
||||
38
server/test/factories/asset-edit.factory.ts
Normal file
38
server/test/factories/asset-edit.factory.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { AssetEditTable } from 'src/schema/tables/asset-edit.table';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { build } from 'test/factories/builder.factory';
|
||||
import { AssetEditLike, AssetLike, FactoryBuilder } from 'test/factories/types';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
|
||||
export class AssetEditFactory {
|
||||
private constructor(private readonly value: Selectable<AssetEditTable>) {}
|
||||
|
||||
static create(dto: AssetEditLike = {}) {
|
||||
return AssetEditFactory.from(dto).build();
|
||||
}
|
||||
|
||||
static from(dto: AssetEditLike = {}) {
|
||||
const id = dto.id ?? newUuid();
|
||||
|
||||
return new AssetEditFactory({
|
||||
id,
|
||||
assetId: newUuid(),
|
||||
action: AssetEditAction.Crop,
|
||||
parameters: { x: 5, y: 6, width: 200, height: 100 },
|
||||
sequence: 1,
|
||||
...dto,
|
||||
});
|
||||
}
|
||||
|
||||
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
|
||||
const asset = build(AssetFactory.from(dto), builder);
|
||||
this.value.assetId = asset.build().id;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return { ...this.value } as Selectable<AssetEditTable<AssetEditAction.Crop>>;
|
||||
}
|
||||
}
|
||||
43
server/test/factories/asset-file.factory.ts
Normal file
43
server/test/factories/asset-file.factory.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { AssetFileType } from 'src/enum';
|
||||
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { build } from 'test/factories/builder.factory';
|
||||
import { AssetFileLike, AssetLike, FactoryBuilder } from 'test/factories/types';
|
||||
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
|
||||
|
||||
export class AssetFileFactory {
|
||||
private constructor(private readonly value: Selectable<AssetFileTable>) {}
|
||||
|
||||
static create(dto: AssetFileLike = {}) {
|
||||
return AssetFileFactory.from(dto).build();
|
||||
}
|
||||
|
||||
static from(dto: AssetFileLike = {}) {
|
||||
const id = dto.id ?? newUuid();
|
||||
const isEdited = dto.isEdited ?? false;
|
||||
|
||||
return new AssetFileFactory({
|
||||
id,
|
||||
assetId: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: `/data/12/34/thumbs/${id.slice(0, 2)}/${id.slice(2, 4)}/${id}${isEdited ? '_edited' : ''}.jpg`,
|
||||
updateId: newUuidV7(),
|
||||
isProgressive: false,
|
||||
isEdited,
|
||||
...dto,
|
||||
});
|
||||
}
|
||||
|
||||
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
|
||||
const asset = build(AssetFactory.from(dto), builder);
|
||||
this.value.assetId = asset.build().id;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return { ...this.value };
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,20 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { AssetEditFactory } from 'test/factories/asset-edit.factory';
|
||||
import { AssetExifFactory } from 'test/factories/asset-exif.factory';
|
||||
import { AssetFileFactory } from 'test/factories/asset-file.factory';
|
||||
import { build } from 'test/factories/builder.factory';
|
||||
import { AssetExifLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types';
|
||||
import { AssetEditLike, AssetExifLike, AssetFileLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { factory, newDate, newSha1, newUuid, newUuidV7 } from 'test/small.factory';
|
||||
import { newDate, newSha1, newUuid, newUuidV7 } from 'test/small.factory';
|
||||
|
||||
export class AssetFactory {
|
||||
#assetExif?: AssetExifFactory;
|
||||
#owner!: UserFactory;
|
||||
#assetExif?: AssetExifFactory;
|
||||
#files: AssetFileFactory[] = [];
|
||||
#edits: AssetEditFactory[] = [];
|
||||
|
||||
private constructor(private readonly value: Selectable<AssetTable>) {
|
||||
value.ownerId ??= newUuid();
|
||||
@ -21,8 +26,12 @@ export class AssetFactory {
|
||||
}
|
||||
|
||||
static from(dto: AssetLike = {}) {
|
||||
const id = dto.id ?? newUuid();
|
||||
|
||||
const originalFileName = dto.originalFileName ?? `IMG_${id}.jpg`;
|
||||
|
||||
return new AssetFactory({
|
||||
id: factory.uuid(),
|
||||
id,
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
deletedAt: null,
|
||||
@ -42,8 +51,8 @@ export class AssetFactory {
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: newDate(),
|
||||
originalFileName: 'IMG_123.jpg',
|
||||
originalPath: `/data/12/34/IMG_123.jpg`,
|
||||
originalFileName,
|
||||
originalPath: `/data/library/${originalFileName}`,
|
||||
ownerId: newUuid(),
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
@ -67,13 +76,51 @@ export class AssetFactory {
|
||||
return this;
|
||||
}
|
||||
|
||||
edit(dto: AssetEditLike = {}, builder?: FactoryBuilder<AssetEditFactory>) {
|
||||
this.#edits.push(build(AssetEditFactory.from(dto).asset(this.value), builder));
|
||||
this.value.isEdited = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
file(dto: AssetFileLike = {}, builder?: FactoryBuilder<AssetFileFactory>) {
|
||||
this.#files.push(build(AssetFileFactory.from(dto).asset(this.value), builder));
|
||||
return this;
|
||||
}
|
||||
|
||||
files(dto?: 'edits'): AssetFactory;
|
||||
files(items: AssetFileLike[], builder?: FactoryBuilder<AssetFileFactory>): AssetFactory;
|
||||
files(items: AssetFileType[], builder?: FactoryBuilder<AssetFileFactory>): AssetFactory;
|
||||
files(dto?: 'edits' | AssetFileLike[] | AssetFileType[], builder?: FactoryBuilder<AssetFileFactory>): AssetFactory {
|
||||
const items: AssetFileLike[] = [];
|
||||
|
||||
if (dto === undefined || dto === 'edits') {
|
||||
items.push(...Object.values(AssetFileType).map((type) => ({ type })));
|
||||
|
||||
if (dto === 'edits') {
|
||||
items.push(...Object.values(AssetFileType).map((type) => ({ type, isEdited: true })));
|
||||
}
|
||||
} else {
|
||||
for (const item of dto) {
|
||||
items.push(typeof item === 'string' ? { type: item as AssetFileType } : item);
|
||||
}
|
||||
}
|
||||
for (const item of items) {
|
||||
this.file(item, builder);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
const exif = this.#assetExif?.build();
|
||||
|
||||
return {
|
||||
...this.value,
|
||||
exifInfo: exif as NonNullable<typeof exif>,
|
||||
owner: this.#owner.build(),
|
||||
exifInfo: exif as NonNullable<typeof exif>,
|
||||
files: this.#files.map((file) => file.build()),
|
||||
edits: this.#edits.map((edit) => edit.build()),
|
||||
faces: [] as Selectable<AssetFaceTable>[],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetEditTable } from 'src/schema/tables/asset-edit.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
@ -10,6 +12,8 @@ export type FactoryBuilder<T, R extends T = T> = (builder: T) => R;
|
||||
|
||||
export type AssetLike = Partial<Selectable<AssetTable>>;
|
||||
export type AssetExifLike = Partial<Selectable<AssetExifTable>>;
|
||||
export type AssetEditLike = Partial<Selectable<AssetEditTable>>;
|
||||
export type AssetFileLike = Partial<Selectable<AssetFileTable>>;
|
||||
export type AlbumLike = Partial<Selectable<AlbumTable>>;
|
||||
export type AlbumUserLike = Partial<Selectable<AlbumUserTable>>;
|
||||
export type SharedLinkLike = Partial<Selectable<SharedLinkTable>>;
|
||||
|
||||
68
server/test/fixtures/album.stub.ts
vendored
68
server/test/fixtures/album.stub.ts
vendored
@ -45,56 +45,6 @@ export const albumStub = {
|
||||
order: AssetOrder.Desc,
|
||||
updateId: '42',
|
||||
}),
|
||||
sharedWithMultiple: Object.freeze({
|
||||
id: 'album-3',
|
||||
albumName: 'Empty album shared with users',
|
||||
description: '',
|
||||
ownerId: authStub.admin.user.id,
|
||||
owner: userStub.admin,
|
||||
assets: [],
|
||||
albumThumbnailAsset: null,
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
albumUsers: [
|
||||
{
|
||||
user: userStub.user1,
|
||||
role: AlbumUserRole.Editor,
|
||||
},
|
||||
{
|
||||
user: userStub.user2,
|
||||
role: AlbumUserRole.Editor,
|
||||
},
|
||||
],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.Desc,
|
||||
updateId: '42',
|
||||
}),
|
||||
sharedWithAdmin: Object.freeze({
|
||||
id: 'album-3',
|
||||
albumName: 'Empty album shared with admin',
|
||||
description: '',
|
||||
ownerId: authStub.user1.user.id,
|
||||
owner: userStub.user1,
|
||||
assets: [],
|
||||
albumThumbnailAsset: null,
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
albumUsers: [
|
||||
{
|
||||
user: userStub.admin,
|
||||
role: AlbumUserRole.Editor,
|
||||
},
|
||||
],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.Desc,
|
||||
updateId: '42',
|
||||
}),
|
||||
oneAsset: Object.freeze({
|
||||
id: 'album-4',
|
||||
albumName: 'Album with one asset',
|
||||
@ -113,24 +63,6 @@ export const albumStub = {
|
||||
order: AssetOrder.Desc,
|
||||
updateId: '42',
|
||||
}),
|
||||
twoAssets: Object.freeze({
|
||||
id: 'album-4a',
|
||||
albumName: 'Album with two assets',
|
||||
description: '',
|
||||
ownerId: authStub.admin.user.id,
|
||||
owner: userStub.admin,
|
||||
assets: [assetStub.image, assetStub.withLocation],
|
||||
albumThumbnailAsset: assetStub.image,
|
||||
albumThumbnailAssetId: assetStub.image.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
albumUsers: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.Desc,
|
||||
updateId: '42',
|
||||
}),
|
||||
emptyWithValidThumbnail: Object.freeze({
|
||||
id: 'album-5',
|
||||
albumName: 'Empty album with valid thumbnail',
|
||||
|
||||
410
server/test/fixtures/asset.stub.ts
vendored
410
server/test/fixtures/asset.stub.ts
vendored
@ -20,45 +20,8 @@ const fullsizeFile = factory.assetFile({
|
||||
path: '/uploads/user-id/fullsize/path.webp',
|
||||
});
|
||||
|
||||
const sidecarFileWithExt = factory.assetFile({
|
||||
type: AssetFileType.Sidecar,
|
||||
path: '/original/path.ext.xmp',
|
||||
});
|
||||
|
||||
const sidecarFileWithoutExt = factory.assetFile({
|
||||
type: AssetFileType.Sidecar,
|
||||
path: '/original/path.xmp',
|
||||
});
|
||||
|
||||
const editedPreviewFile = factory.assetFile({
|
||||
type: AssetFileType.Preview,
|
||||
path: '/uploads/user-id/preview/path_edited.jpg',
|
||||
isEdited: true,
|
||||
});
|
||||
|
||||
const editedThumbnailFile = factory.assetFile({
|
||||
type: AssetFileType.Thumbnail,
|
||||
path: '/uploads/user-id/thumbnail/path_edited.jpg',
|
||||
isEdited: true,
|
||||
});
|
||||
|
||||
const editedFullsizeFile = factory.assetFile({
|
||||
type: AssetFileType.FullSize,
|
||||
path: '/uploads/user-id/fullsize/path_edited.jpg',
|
||||
isEdited: true,
|
||||
});
|
||||
|
||||
const files = [fullsizeFile, previewFile, thumbnailFile];
|
||||
|
||||
const editedFiles = [
|
||||
fullsizeFile,
|
||||
previewFile,
|
||||
thumbnailFile,
|
||||
editedFullsizeFile,
|
||||
editedPreviewFile,
|
||||
editedThumbnailFile,
|
||||
];
|
||||
|
||||
export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => {
|
||||
return {
|
||||
id: stackId,
|
||||
@ -132,87 +95,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
noWebpPath: 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: '/data/library/IMG_456.jpg',
|
||||
files: [previewFile],
|
||||
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: [],
|
||||
originalFileName: 'IMG_456.jpg',
|
||||
faces: [],
|
||||
isExternal: false,
|
||||
exifInfo: {
|
||||
fileSizeInByte: 123_000,
|
||||
} as Exif,
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
updateId: '42',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
noThumbhash: 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.ext',
|
||||
files,
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
thumbhash: null,
|
||||
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,
|
||||
isExternal: false,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.ext',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
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',
|
||||
status: AssetStatus.Active,
|
||||
@ -526,48 +408,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
imageFrom2015: Object.freeze({
|
||||
id: 'asset-id-2015',
|
||||
status: AssetStatus.Active,
|
||||
deviceAssetId: 'device-asset-id',
|
||||
fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'),
|
||||
fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'),
|
||||
owner: userStub.user1,
|
||||
ownerId: 'user-id',
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/original/path.ext',
|
||||
files,
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2015-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2015-02-23T05:06:29.716Z'),
|
||||
localDateTime: new Date('2015-02-23T05:06:29.716Z'),
|
||||
isFavorite: true,
|
||||
isExternal: false,
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
updateId: 'foo',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.ext',
|
||||
faces: [],
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
} as Exif,
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
video: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
@ -736,81 +576,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
sidecar: 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.ext',
|
||||
thumbhash: null,
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
files: [previewFile, sidecarFileWithExt],
|
||||
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,
|
||||
isExternal: false,
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.ext',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
updateId: 'foo',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
sidecarWithoutExt: 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.ext',
|
||||
thumbhash: null,
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
files: [previewFile, sidecarFileWithoutExt],
|
||||
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,
|
||||
isExternal: false,
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.ext',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
hasEncodedVideo: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
@ -854,46 +619,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
hasFileExtension: 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: '/data/user1/photo.jpg',
|
||||
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'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: true,
|
||||
isExternal: true,
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: 'library-id',
|
||||
sharedLinks: [],
|
||||
originalFileName: 'photo.jpg',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
imageDng: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
@ -938,93 +663,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
imageHif: 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.hif',
|
||||
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'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: true,
|
||||
duration: null,
|
||||
isExternal: false,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.hif',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
profileDescription: 'Adobe RGB',
|
||||
bitsPerSample: 14,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
updateId: '42',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
panoramaTif: 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.tif',
|
||||
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'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: true,
|
||||
duration: null,
|
||||
isExternal: false,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.tif',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
sidecarPath: null,
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
projectionType: 'EQUIRECTANGULAR',
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
updateId: '42',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
}),
|
||||
|
||||
withCropEdit: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
@ -1082,52 +720,4 @@ export const assetStub = {
|
||||
] as AssetEditActionItem[],
|
||||
isEdited: true,
|
||||
}),
|
||||
|
||||
withoutEdits: 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: editedFiles,
|
||||
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,
|
||||
sidecarPath: null,
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
exifImageHeight: 3840,
|
||||
exifImageWidth: 2160,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
stack: null,
|
||||
orientation: '',
|
||||
projectionType: null,
|
||||
height: 3840,
|
||||
width: 2160,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
};
|
||||
|
||||
17
server/test/fixtures/user.stub.ts
vendored
17
server/test/fixtures/user.stub.ts
vendored
@ -38,21 +38,4 @@ export const userStub = {
|
||||
quotaSizeInBytes: null,
|
||||
quotaUsageInBytes: 0,
|
||||
},
|
||||
user2: <UserAdmin>{
|
||||
...authStub.user2.user,
|
||||
status: UserStatus.Active,
|
||||
profileChangedAt: new Date('2021-01-01'),
|
||||
metadata: [],
|
||||
name: 'immich_name',
|
||||
storageLabel: null,
|
||||
oauthId: '',
|
||||
shouldChangePassword: false,
|
||||
avatarColor: null,
|
||||
profileImagePath: '',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
quotaSizeInBytes: null,
|
||||
quotaUsageInBytes: 0,
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user