From e3e243fa2b9627c5e516d4fd2402637bb28045d8 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 6 Feb 2026 18:47:54 -0500 Subject: [PATCH] refactor: tests (#25987) --- server/src/dtos/album-response.dto.spec.ts | 13 +- server/src/services/asset.service.spec.ts | 96 ++--- server/src/services/download.service.spec.ts | 33 +- server/src/services/media.service.spec.ts | 119 +++--- server/src/services/media.service.ts | 2 +- server/src/services/metadata.service.spec.ts | 267 ++++++------ server/src/services/stack.service.spec.ts | 7 +- server/test/factories/asset-edit.factory.ts | 38 ++ server/test/factories/asset-file.factory.ts | 43 ++ server/test/factories/asset.factory.ts | 63 ++- server/test/factories/types.ts | 4 + server/test/fixtures/album.stub.ts | 68 --- server/test/fixtures/asset.stub.ts | 410 ------------------- server/test/fixtures/user.stub.ts | 17 - 14 files changed, 415 insertions(+), 765 deletions(-) create mode 100644 server/test/factories/asset-edit.factory.ts create mode 100644 server/test/factories/asset-file.factory.ts diff --git a/server/src/dtos/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts index dd8642598f..d3536a3482 100644 --- a/server/src/dtos/album-response.dto.spec.ts +++ b/server/src/dtos/album-response.dto.spec.ts @@ -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(); }); diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 707faa326d..ff4dfa96ff 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -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, diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 86d0bda7f8..7721b12ffc 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -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 () => { diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 75812e2fcb..bee1ed67d9 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -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 })); }); }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 00bd0305dd..2c4005f436 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -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 ?? [], ); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index eda4e1a063..d94de020e0 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -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 } }]); }); }); diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index 5517cf17f8..1dc87f4348 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -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(); diff --git a/server/test/factories/asset-edit.factory.ts b/server/test/factories/asset-edit.factory.ts new file mode 100644 index 0000000000..e16b0c2e4b --- /dev/null +++ b/server/test/factories/asset-edit.factory.ts @@ -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) {} + + 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) { + const asset = build(AssetFactory.from(dto), builder); + this.value.assetId = asset.build().id; + return this; + } + + build() { + return { ...this.value } as Selectable>; + } +} diff --git a/server/test/factories/asset-file.factory.ts b/server/test/factories/asset-file.factory.ts new file mode 100644 index 0000000000..109cd5adc4 --- /dev/null +++ b/server/test/factories/asset-file.factory.ts @@ -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) {} + + 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) { + const asset = build(AssetFactory.from(dto), builder); + this.value.assetId = asset.build().id; + return this; + } + + build() { + return { ...this.value }; + } +} diff --git a/server/test/factories/asset.factory.ts b/server/test/factories/asset.factory.ts index 41714dbf67..8cbf704abf 100644 --- a/server/test/factories/asset.factory.ts +++ b/server/test/factories/asset.factory.ts @@ -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) { 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) { + this.#edits.push(build(AssetEditFactory.from(dto).asset(this.value), builder)); + this.value.isEdited = true; + return this; + } + + file(dto: AssetFileLike = {}, builder?: FactoryBuilder) { + this.#files.push(build(AssetFileFactory.from(dto).asset(this.value), builder)); + return this; + } + + files(dto?: 'edits'): AssetFactory; + files(items: AssetFileLike[], builder?: FactoryBuilder): AssetFactory; + files(items: AssetFileType[], builder?: FactoryBuilder): AssetFactory; + files(dto?: 'edits' | AssetFileLike[] | AssetFileType[], builder?: FactoryBuilder): 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, owner: this.#owner.build(), + exifInfo: exif as NonNullable, + files: this.#files.map((file) => file.build()), + edits: this.#edits.map((edit) => edit.build()), + faces: [] as Selectable[], }; } } diff --git a/server/test/factories/types.ts b/server/test/factories/types.ts index 43ae26c9f4..534e290f59 100644 --- a/server/test/factories/types.ts +++ b/server/test/factories/types.ts @@ -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 = (builder: T) => R; export type AssetLike = Partial>; export type AssetExifLike = Partial>; +export type AssetEditLike = Partial>; +export type AssetFileLike = Partial>; export type AlbumLike = Partial>; export type AlbumUserLike = Partial>; export type SharedLinkLike = Partial>; diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index d36989bbcf..9480fdd5ab 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -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', diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 05219c92e7..3c89056f37 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -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, - }), }; diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 807da5197f..21b49ab899 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -38,21 +38,4 @@ export const userStub = { quotaSizeInBytes: null, quotaUsageInBytes: 0, }, - user2: { - ...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, - }, };