diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 5317989739..fc825fb273 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -23,10 +23,11 @@ import { MediaService } from 'src/services/media.service'; import { JobCounts, RawImageInfo } from 'src/types'; import { AssetFaceFactory } from 'test/factories/asset-face.factory'; import { AssetFactory } from 'test/factories/asset.factory'; +import { PersonFactory } from 'test/factories/person.factory'; import { probeStub } from 'test/fixtures/media.stub'; -import { personStub, personThumbnailStub } from 'test/fixtures/person.stub'; +import { personThumbnailStub } from 'test/fixtures/person.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; -import { factory } from 'test/small.factory'; +import { factory, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; const fullsizeBuffer = Buffer.from('embedded image data'); @@ -50,9 +51,10 @@ describe(MediaService.name, () => { describe('handleQueueGenerateThumbnails', () => { it('should queue all assets', async () => { const asset = AssetFactory.create(); + const person = PersonFactory.create({ faceAssetId: newUuid() }); mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); - mocks.person.getAll.mockReturnValue(makeStream([personStub.newThumbnail])); + mocks.person.getAll.mockReturnValue(makeStream([person])); await sut.handleQueueGenerateThumbnails({ force: true }); @@ -68,7 +70,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.newThumbnail.id }, + data: { id: person.id }, }, ]); }); @@ -106,8 +108,13 @@ describe(MediaService.name, () => { }); it('should queue all people with missing thumbnail path', async () => { + const [person1, person2] = [ + PersonFactory.create({ thumbnailPath: undefined }), + PersonFactory.create({ thumbnailPath: undefined }), + ]; + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([AssetFactory.create()])); - mocks.person.getAll.mockReturnValue(makeStream([personStub.noThumbnail, personStub.noThumbnail])); + mocks.person.getAll.mockReturnValue(makeStream([person1, person2])); mocks.person.getRandomFace.mockResolvedValueOnce(AssetFaceFactory.create()); await sut.handleQueueGenerateThumbnails({ force: false }); @@ -120,7 +127,7 @@ describe(MediaService.name, () => { { name: JobName.PersonGenerateThumbnail, data: { - id: personStub.newThumbnail.id, + id: person1.id, }, }, ]); @@ -276,17 +283,17 @@ describe(MediaService.name, () => { describe('handleQueueMigration', () => { it('should remove empty directories and queue jobs', async () => { const asset = AssetFactory.create(); + const person = PersonFactory.create(); + mocks.assetJob.streamForMigrationJob.mockReturnValue(makeStream([asset])); mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts); - mocks.person.getAll.mockReturnValue(makeStream([personStub.withName])); + mocks.person.getAll.mockReturnValue(makeStream([person])); await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.Success); expect(mocks.storage.removeEmptyDirs).toHaveBeenCalledTimes(2); expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.AssetFileMigration, data: { id: asset.id } }]); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.PersonFileMigration, data: { id: personStub.withName.id } }, - ]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.PersonFileMigration, data: { id: person.id } }]); }); }); @@ -1479,8 +1486,9 @@ describe(MediaService.name, () => { }); it('should skip a person without a face asset id', async () => { - mocks.person.getById.mockResolvedValue(personStub.noThumbnail); - await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); + const person = PersonFactory.create({ faceAssetId: null }); + mocks.person.getById.mockResolvedValue(person); + await sut.handleGeneratePersonThumbnail({ id: person.id }); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); }); @@ -1490,17 +1498,17 @@ describe(MediaService.name, () => { }); it('should generate a thumbnail', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailMiddle); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); - expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id); + expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(person.id); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.originalPath, { colorspace: Colorspace.P3, @@ -1531,21 +1539,21 @@ describe(MediaService.name, () => { }, expect.any(String), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', thumbnailPath: expect.any(String) }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, thumbnailPath: expect.any(String) }); }); it('should use preview path if video', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.videoThumbnail); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); - expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id); + expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(person.id); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledWith(expect.any(String), { colorspace: Colorspace.P3, @@ -1576,19 +1584,19 @@ describe(MediaService.name, () => { }, expect.any(String), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', thumbnailPath: expect.any(String) }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, thumbnailPath: expect.any(String) }); }); it('should generate a thumbnail without going negative', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailStart); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailStart.originalPath, { colorspace: Colorspace.P3, @@ -1622,16 +1630,16 @@ describe(MediaService.name, () => { }); it('should generate a thumbnail without overflowing', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailEnd); - mocks.person.update.mockResolvedValue(personStub.primaryPerson); + mocks.person.update.mockResolvedValue(person); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailEnd.originalPath, { colorspace: Colorspace.P3, @@ -1665,16 +1673,16 @@ describe(MediaService.name, () => { }); it('should handle negative coordinates', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.negativeCoordinate); - mocks.person.update.mockResolvedValue(personStub.primaryPerson); + mocks.person.update.mockResolvedValue(person); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 4624, height: 3080 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.negativeCoordinate.originalPath, { colorspace: Colorspace.P3, @@ -1708,16 +1716,16 @@ describe(MediaService.name, () => { }); it('should handle overflowing coordinate', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.overflowingCoordinate); - mocks.person.update.mockResolvedValue(personStub.primaryPerson); + mocks.person.update.mockResolvedValue(person); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 4624, height: 3080 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.overflowingCoordinate.originalPath, { colorspace: Colorspace.P3, @@ -1751,9 +1759,11 @@ describe(MediaService.name, () => { }); it('should use embedded preview if enabled and raw image', async () => { + const person = PersonFactory.create(); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail); - mocks.person.update.mockResolvedValue(personStub.primaryPerson); + mocks.person.update.mockResolvedValue(person); mocks.media.generateThumbnail.mockResolvedValue(); const extracted = Buffer.from(''); const data = Buffer.from(''); @@ -1762,9 +1772,7 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); mocks.media.getImageMetadata.mockResolvedValue({ width: 2160, height: 3840, isTransparent: false }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extracted, { @@ -1799,21 +1807,23 @@ describe(MediaService.name, () => { }); it('should not use embedded preview if enabled and not raw image', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailMiddle); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).not.toHaveBeenCalled(); expect(mocks.media.generateThumbnail).toHaveBeenCalled(); }); it('should not use embedded preview if enabled and raw image if not exists', async () => { + const person = PersonFactory.create(); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail); mocks.media.generateThumbnail.mockResolvedValue(); @@ -1821,9 +1831,7 @@ describe(MediaService.name, () => { const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath, { @@ -1835,6 +1843,8 @@ describe(MediaService.name, () => { }); it('should not use embedded preview if enabled and raw image if low resolution', async () => { + const person = PersonFactory.create(); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail); mocks.media.generateThumbnail.mockResolvedValue(); @@ -1845,9 +1855,7 @@ describe(MediaService.name, () => { mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg }); mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath, { diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 1080407922..feaba36b1d 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -16,8 +16,8 @@ import { import { ImmichTags } from 'src/repositories/metadata.repository'; import { firstDateTime, MetadataService } from 'src/services/metadata.service'; import { AssetFactory } from 'test/factories/asset.factory'; +import { PersonFactory } from 'test/factories/person.factory'; import { probeStub } from 'test/fixtures/media.stub'; -import { personStub } from 'test/fixtures/person.stub'; import { tagStub } from 'test/fixtures/tag.stub'; import { factory } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; @@ -1208,18 +1208,18 @@ describe(MetadataService.name, () => { it('should apply metadata face tags creating new people', async () => { const asset = AssetFactory.create(); + const person = PersonFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); - mockReadTags(makeFaceTags({ Name: personStub.withName.name })); + mockReadTags(makeFaceTags({ Name: person.name })); mocks.person.getDistinctNames.mockResolvedValue([]); - mocks.person.createAll.mockResolvedValue([personStub.withName.id]); - mocks.person.update.mockResolvedValue(personStub.withName); + mocks.person.createAll.mockResolvedValue([person.id]); + mocks.person.update.mockResolvedValue(person); await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true }); - expect(mocks.person.createAll).toHaveBeenCalledWith([ - expect.objectContaining({ name: personStub.withName.name }), - ]); + expect(mocks.person.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: person.name })]); expect(mocks.person.refreshFaces).toHaveBeenCalledWith( [ { @@ -1243,19 +1243,21 @@ describe(MetadataService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.withName.id }, + data: { id: person.id }, }, ]); }); it('should assign metadata face tags to existing persons', async () => { const asset = AssetFactory.create(); + const person = PersonFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); - mockReadTags(makeFaceTags({ Name: personStub.withName.name })); - mocks.person.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]); + mockReadTags(makeFaceTags({ Name: person.name })); + mocks.person.getDistinctNames.mockResolvedValue([{ id: person.id, name: person.name }]); mocks.person.createAll.mockResolvedValue([]); - mocks.person.update.mockResolvedValue(personStub.withName); + mocks.person.update.mockResolvedValue(person); await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true }); @@ -1265,7 +1267,7 @@ describe(MetadataService.name, () => { { id: 'random-uuid', assetId: asset.id, - personId: personStub.withName.id, + personId: person.id, imageHeight: 100, imageWidth: 1000, boundingBoxX1: 0, @@ -1335,21 +1337,20 @@ describe(MetadataService.name, () => { async ({ orientation, expected }) => { const { imgW, imgH, x1, x2, y1, y2 } = expected; const asset = AssetFactory.create(); + const person = PersonFactory.create(); mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); - mockReadTags(makeFaceTags({ Name: personStub.withName.name }, orientation)); + mockReadTags(makeFaceTags({ Name: person.name }, orientation)); mocks.person.getDistinctNames.mockResolvedValue([]); - mocks.person.createAll.mockResolvedValue([personStub.withName.id]); - mocks.person.update.mockResolvedValue(personStub.withName); + mocks.person.createAll.mockResolvedValue([person.id]); + mocks.person.update.mockResolvedValue(person); await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true, }); - expect(mocks.person.createAll).toHaveBeenCalledWith([ - expect.objectContaining({ name: personStub.withName.name }), - ]); + expect(mocks.person.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: person.name })]); expect(mocks.person.refreshFaces).toHaveBeenCalledWith( [ { @@ -1373,7 +1374,7 @@ describe(MetadataService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.withName.id }, + data: { id: person.id }, }, ]); }, diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index d7c9fa9f59..c22fd65a1a 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -1,6 +1,6 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { mapFaces, mapPerson, PersonResponseDto } from 'src/dtos/person.dto'; +import { mapFaces, mapPerson } from 'src/dtos/person.dto'; import { AssetFileType, CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum'; import { FaceSearchResult } from 'src/repositories/search.repository'; import { PersonService } from 'src/services/person.service'; @@ -11,25 +11,11 @@ import { AuthFactory } from 'test/factories/auth.factory'; import { PersonFactory } from 'test/factories/person.factory'; import { UserFactory } from 'test/factories/user.factory'; import { authStub } from 'test/fixtures/auth.stub'; -import { personStub } from 'test/fixtures/person.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { getAsDetectedFace, getForFacialRecognitionJob } from 'test/mappers'; import { newDate, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; -const responseDto: PersonResponseDto = { - id: 'person-1', - name: 'Person 1', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - isHidden: false, - updatedAt: expect.any(Date), - isFavorite: false, - color: expect.any(String), -}; - -const statistics = { assets: 3 }; - describe(PersonService.name, () => { let sut: PersonService; let mocks: ServiceMocks; @@ -44,60 +30,54 @@ describe(PersonService.name, () => { describe('getAll', () => { it('should get all hidden and visible people with thumbnails', async () => { + const auth = AuthFactory.create(); + const [person, hiddenPerson] = [PersonFactory.create(), PersonFactory.create({ isHidden: true })]; + mocks.person.getAllForUser.mockResolvedValue({ - items: [personStub.withName, personStub.hidden], + items: [person, hiddenPerson], hasNextPage: false, }); mocks.person.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 }); - await expect(sut.getAll(authStub.admin, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({ + await expect(sut.getAll(auth, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({ hasNextPage: false, total: 2, hidden: 1, people: [ - responseDto, - { - id: 'person-1', - name: '', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', + expect.objectContaining({ id: person.id, isHidden: false }), + expect.objectContaining({ + id: hiddenPerson.id, isHidden: true, - isFavorite: false, - updatedAt: expect.any(Date), - color: expect.any(String), - }, + }), ], }); - expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, { + expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, { minimumFaceCount: 3, withHidden: true, }); }); it('should get all visible people and favorites should be first in the array', async () => { + const auth = AuthFactory.create(); + const [isFavorite, person] = [PersonFactory.create({ isFavorite: true }), PersonFactory.create()]; + mocks.person.getAllForUser.mockResolvedValue({ - items: [personStub.isFavorite, personStub.withName], + items: [isFavorite, person], hasNextPage: false, }); mocks.person.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 }); - await expect(sut.getAll(authStub.admin, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({ + await expect(sut.getAll(auth, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({ hasNextPage: false, total: 2, hidden: 1, people: [ - { - id: 'person-4', - name: personStub.isFavorite.name, - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - isHidden: false, + expect.objectContaining({ + id: isFavorite.id, isFavorite: true, - updatedAt: expect.any(Date), - color: personStub.isFavorite.color, - }, - responseDto, + }), + expect.objectContaining({ id: person.id, isFavorite: false }), ], }); - expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, { + expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, { minimumFaceCount: 3, withHidden: false, }); @@ -106,71 +86,89 @@ describe(PersonService.name, () => { describe('getById', () => { it('should require person.read permission', async () => { - mocks.person.getById.mockResolvedValue(personStub.withName); - await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + mocks.person.getById.mockResolvedValue(person); + await expect(sut.getById(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw a bad request when person is not found', async () => { - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['unknown'])); + await expect(sut.getById(auth, 'unknown')).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['unknown'])); }); it('should get a person by id', async () => { - mocks.person.getById.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto); - expect(mocks.person.getById).toHaveBeenCalledWith('person-1'); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + await expect(sut.getById(auth, person.id)).resolves.toEqual(expect.objectContaining({ id: person.id })); + expect(mocks.person.getById).toHaveBeenCalledWith(person.id); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); describe('getThumbnail', () => { it('should require person.read permission', async () => { - mocks.person.getById.mockResolvedValue(personStub.noName); - await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + await expect(sut.getThumbnail(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.storage.createReadStream).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw an error when personId is invalid', async () => { - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); + const auth = AuthFactory.create(); + + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['unknown'])); + await expect(sut.getThumbnail(auth, 'unknown')).rejects.toBeInstanceOf(NotFoundException); expect(mocks.storage.createReadStream).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['unknown'])); }); it('should throw an error when person has no thumbnail', async () => { - mocks.person.getById.mockResolvedValue(personStub.noThumbnail); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ thumbnailPath: '' }); + + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + await expect(sut.getThumbnail(auth, person.id)).rejects.toBeInstanceOf(NotFoundException); expect(mocks.storage.createReadStream).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should serve the thumbnail', async () => { - mocks.person.getById.mockResolvedValue(personStub.noName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getThumbnail(authStub.admin, 'person-1')).resolves.toEqual( + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + await expect(sut.getThumbnail(auth, person.id)).resolves.toEqual( new ImmichFileResponse({ - path: '/path/to/thumbnail.jpg', + path: person.thumbnailPath, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithoutCache, }), ); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); describe('update', () => { it('should require person.write permission', async () => { - mocks.person.getById.mockResolvedValue(personStub.noName); - await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( - BadRequestException, - ); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + await expect(sut.update(auth, person.id, { name: 'Person 1' })).rejects.toBeInstanceOf(BadRequestException); expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw an error when personId is invalid', async () => { @@ -183,86 +181,108 @@ describe(PersonService.name, () => { }); it("should update a person's name", async () => { - mocks.person.update.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ name: 'Person 1' }); - await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto); + mocks.person.update.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + await expect(sut.update(auth, person.id, { name: 'Person 1' })).resolves.toEqual( + expect.objectContaining({ id: person.id, name: 'Person 1' }), + ); + + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, name: 'Person 1' }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it("should update a person's date of birth", async () => { - mocks.person.update.mockResolvedValue(personStub.withBirthDate); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ birthDate: new Date('1976-06-30') }); - await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({ - id: 'person-1', - name: 'Person 1', + mocks.person.update.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + + await expect(sut.update(auth, person.id, { birthDate: new Date('1976-06-30') })).resolves.toEqual({ + id: person.id, + name: person.name, birthDate: '1976-06-30', - thumbnailPath: '/path/to/thumbnail.jpg', + thumbnailPath: person.thumbnailPath, isHidden: false, isFavorite: false, updatedAt: expect.any(Date), - color: expect.any(String), }); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: new Date('1976-06-30') }); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should update a person visibility', async () => { - mocks.person.update.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ isHidden: true }); - await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto); + mocks.person.update.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + await expect(sut.update(auth, person.id, { isHidden: true })).resolves.toEqual( + expect.objectContaining({ isHidden: true }), + ); + + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, isHidden: true }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should update a person favorite status', async () => { - mocks.person.update.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ isFavorite: true }); - await expect(sut.update(authStub.admin, 'person-1', { isFavorite: true })).resolves.toEqual(responseDto); + mocks.person.update.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', isFavorite: true }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + await expect(sut.update(auth, person.id, { isFavorite: true })).resolves.toEqual( + expect.objectContaining({ isFavorite: true }), + ); + + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, isFavorite: true }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it("should update a person's thumbnailPath", async () => { const face = AssetFaceFactory.create(); const auth = AuthFactory.create(); - mocks.person.update.mockResolvedValue(personStub.withName); + const person = PersonFactory.create(); + + mocks.person.update.mockResolvedValue(person); mocks.person.getForFeatureFaceUpdate.mockResolvedValue(face); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([face.assetId])); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - await expect(sut.update(auth, 'person-1', { featureFaceAssetId: face.assetId })).resolves.toEqual(responseDto); + await expect(sut.update(auth, person.id, { featureFaceAssetId: face.assetId })).resolves.toEqual( + expect.objectContaining({ id: person.id }), + ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: face.id }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, faceAssetId: face.id }); expect(mocks.person.getForFeatureFaceUpdate).toHaveBeenCalledWith({ assetId: face.assetId, - personId: 'person-1', + personId: person.id, }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.PersonGenerateThumbnail, - data: { id: 'person-1' }, + data: { id: person.id }, }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw an error when the face feature assetId is invalid', async () => { - mocks.person.getById.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); - await expect(sut.update(authStub.admin, 'person-1', { featureFaceAssetId: '-1' })).rejects.toThrow( - BadRequestException, - ); + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + + await expect(sut.update(auth, person.id, { featureFaceAssetId: '-1' })).rejects.toThrow(BadRequestException); expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); @@ -283,36 +303,39 @@ describe(PersonService.name, () => { mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set()); await expect( - sut.reassignFaces(authStub.admin, personStub.noName.id, { + sut.reassignFaces(AuthFactory.create(), 'person-id', { data: [{ personId: 'asset-face-1', assetId: '' }], }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.job.queue).not.toHaveBeenCalledWith(); expect(mocks.job.queueAll).not.toHaveBeenCalledWith(); }); + it('should reassign a face', async () => { const face = AssetFaceFactory.create(); const auth = AuthFactory.create(); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.withName.id])); - mocks.person.getById.mockResolvedValue(personStub.noName); + const person = PersonFactory.create(); + + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.person.getById.mockResolvedValue(person); mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id])); mocks.person.getFacesByIds.mockResolvedValue([face]); mocks.person.reassignFace.mockResolvedValue(1); mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create()); mocks.person.refreshFaces.mockResolvedValue(); mocks.person.reassignFace.mockResolvedValue(5); - mocks.person.update.mockResolvedValue(personStub.noName); + mocks.person.update.mockResolvedValue(person); await expect( - sut.reassignFaces(auth, personStub.noName.id, { - data: [{ personId: personStub.withName.id, assetId: face.assetId }], + sut.reassignFaces(auth, person.id, { + data: [{ personId: person.id, assetId: face.assetId }], }), ).resolves.toBeDefined(); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.newThumbnail.id }, + data: { id: person.id }, }, ]); }); @@ -320,7 +343,7 @@ describe(PersonService.name, () => { describe('handlePersonMigration', () => { it('should not move person files', async () => { - await expect(sut.handlePersonMigration(personStub.noName)).resolves.toBe(JobStatus.Failed); + await expect(sut.handlePersonMigration(PersonFactory.create())).resolves.toBe(JobStatus.Failed); }); }); @@ -347,12 +370,14 @@ describe(PersonService.name, () => { describe('createNewFeaturePhoto', () => { it('should change person feature photo', async () => { + const person = PersonFactory.create(); + mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create()); - await sut.createNewFeaturePhoto([personStub.newThumbnail.id]); + await sut.createNewFeaturePhoto([person.id]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.newThumbnail.id }, + data: { id: person.id }, }, ]); }); @@ -361,23 +386,22 @@ describe(PersonService.name, () => { describe('reassignFacesById', () => { it('should create a new person', async () => { const face = AssetFaceFactory.create(); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); + const person = PersonFactory.create(); + + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id])); mocks.person.getFaceById.mockResolvedValue(face); mocks.person.reassignFace.mockResolvedValue(1); - mocks.person.getById.mockResolvedValue(personStub.noName); - await expect(sut.reassignFacesById(AuthFactory.create(), personStub.noName.id, { id: face.id })).resolves.toEqual( - { - birthDate: personStub.noName.birthDate, - isHidden: personStub.noName.isHidden, - isFavorite: personStub.noName.isFavorite, - id: personStub.noName.id, - name: personStub.noName.name, - thumbnailPath: personStub.noName.thumbnailPath, - updatedAt: expect.any(Date), - color: personStub.noName.color, - }, - ); + mocks.person.getById.mockResolvedValue(person); + await expect(sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id })).resolves.toEqual({ + birthDate: person.birthDate, + isHidden: person.isHidden, + isFavorite: person.isFavorite, + id: person.id, + name: person.name, + thumbnailPath: person.thumbnailPath, + updatedAt: expect.any(Date), + }); expect(mocks.job.queue).not.toHaveBeenCalledWith(); expect(mocks.job.queueAll).not.toHaveBeenCalledWith(); @@ -385,12 +409,14 @@ describe(PersonService.name, () => { it('should fail if user has not the correct permissions on the asset', async () => { const face = AssetFaceFactory.create(); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); + const person = PersonFactory.create(); + + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); mocks.person.getFaceById.mockResolvedValue(face); mocks.person.reassignFace.mockResolvedValue(1); - mocks.person.getById.mockResolvedValue(personStub.noName); + mocks.person.getById.mockResolvedValue(person); await expect( - sut.reassignFacesById(AuthFactory.create(), personStub.noName.id, { + sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id, }), ).rejects.toBeInstanceOf(BadRequestException); @@ -402,22 +428,25 @@ describe(PersonService.name, () => { describe('createPerson', () => { it('should create a new person', async () => { - mocks.person.create.mockResolvedValue(personStub.primaryPerson); + const auth = AuthFactory.create(); - await expect(sut.create(authStub.admin, {})).resolves.toBeDefined(); + mocks.person.create.mockResolvedValue(PersonFactory.create()); + await expect(sut.create(auth, {})).resolves.toBeDefined(); - expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id }); + expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: auth.user.id }); }); }); describe('handlePersonCleanup', () => { it('should delete people without faces', async () => { - mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.noName]); + const person = PersonFactory.create(); + + mocks.person.getAllWithoutFaces.mockResolvedValue([person]); await sut.handlePersonCleanup(); - expect(mocks.person.delete).toHaveBeenCalledWith([personStub.noName.id]); - expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.noName.thumbnailPath); + expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); }); }); @@ -449,15 +478,17 @@ describe(PersonService.name, () => { it('should queue all assets', async () => { const asset = AssetFactory.create(); + const person = PersonFactory.create(); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); - mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.withName]); + mocks.person.getAllWithoutFaces.mockResolvedValue([person]); await sut.handleQueueDetectFaces({ force: true }); expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MachineLearning }); - expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]); + expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); - expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { @@ -490,10 +521,12 @@ describe(PersonService.name, () => { it('should delete existing people and faces if forced', async () => { const asset = AssetFactory.create(); const face = AssetFaceFactory.from().person().build(); - mocks.person.getAll.mockReturnValue(makeStream([face.person!, personStub.randomPerson])); + const person = PersonFactory.create(); + + mocks.person.getAll.mockReturnValue(makeStream([face.person!, person])); mocks.person.getAllFaces.mockReturnValue(makeStream([face])); mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); - mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); + mocks.person.getAllWithoutFaces.mockResolvedValue([person]); mocks.person.deleteFaces.mockResolvedValue(); await sut.handleQueueDetectFaces({ force: true }); @@ -505,8 +538,8 @@ describe(PersonService.name, () => { data: { id: asset.id }, }, ]); - expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); - expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); + expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); }); }); @@ -661,6 +694,8 @@ describe(PersonService.name, () => { it('should delete existing people if forced', async () => { const face = AssetFaceFactory.from().person().build(); + const person = PersonFactory.create(); + mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, @@ -669,9 +704,9 @@ describe(PersonService.name, () => { failed: 0, delayed: 0, }); - mocks.person.getAll.mockReturnValue(makeStream([face.person!, personStub.randomPerson])); + mocks.person.getAll.mockReturnValue(makeStream([face.person!, person])); mocks.person.getAllFaces.mockReturnValue(makeStream([face])); - mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); + mocks.person.getAllWithoutFaces.mockResolvedValue([person]); mocks.person.unassignFaces.mockResolvedValue(); await sut.handleQueueRecognizeFaces({ force: true }); @@ -684,8 +719,8 @@ describe(PersonService.name, () => { data: { id: face.id, deferred: false }, }, ]); - expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); - expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); + expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false }); }); }); @@ -1059,59 +1094,71 @@ describe(PersonService.name, () => { describe('mergePerson', () => { it('should require person.write and person.merge permission', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson); + const auth = AuthFactory.create(); + const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( + mocks.person.getById.mockResolvedValueOnce(person); + mocks.person.getById.mockResolvedValueOnce(mergePerson); + + await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).rejects.toBeInstanceOf( BadRequestException, ); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should merge two people without smart merge', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); + const auth = AuthFactory.create(); + const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ - { id: 'person-2', success: true }, + mocks.person.getById.mockResolvedValueOnce(person); + mocks.person.getById.mockResolvedValueOnce(mergePerson); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); + + await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ + { id: mergePerson.id, success: true }, ]); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - newPersonId: personStub.primaryPerson.id, - oldPersonId: personStub.mergePerson.id, + newPersonId: person.id, + oldPersonId: mergePerson.id, }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should merge two people with smart merge', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.randomPerson); - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.person.update.mockResolvedValue({ ...personStub.randomPerson, name: personStub.primaryPerson.name }); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3'])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + const auth = AuthFactory.create(); + const [person, mergePerson] = [ + PersonFactory.create({ name: undefined }), + PersonFactory.create({ name: 'Merge person' }), + ]; - await expect(sut.mergePerson(authStub.admin, 'person-3', { ids: ['person-1'] })).resolves.toEqual([ - { id: 'person-1', success: true }, + mocks.person.getById.mockResolvedValueOnce(person); + mocks.person.getById.mockResolvedValueOnce(mergePerson); + mocks.person.update.mockResolvedValue({ ...person, name: mergePerson.name }); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); + + await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ + { id: mergePerson.id, success: true }, ]); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - newPersonId: personStub.randomPerson.id, - oldPersonId: personStub.primaryPerson.id, + newPersonId: person.id, + oldPersonId: mergePerson.id, }); expect(mocks.person.update).toHaveBeenCalledWith({ - id: personStub.randomPerson.id, - name: personStub.primaryPerson.name, + id: person.id, + name: mergePerson.name, }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw an error when the primary person is not found', async () => { @@ -1126,48 +1173,60 @@ describe(PersonService.name, () => { }); it('should handle invalid merge ids', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ - { id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND }, + mocks.person.getById.mockResolvedValueOnce(person); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['unknown'])); + + await expect(sut.mergePerson(auth, person.id, { ids: ['unknown'] })).resolves.toEqual([ + { id: 'unknown', success: false, error: BulkIdErrorReason.NOT_FOUND }, ]); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should handle an error reassigning faces', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson); - mocks.person.reassignFaces.mockRejectedValue(new Error('update failed')); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); + const auth = AuthFactory.create(); + const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ - { id: 'person-2', success: false, error: BulkIdErrorReason.UNKNOWN }, + mocks.person.getById.mockResolvedValueOnce(person); + mocks.person.getById.mockResolvedValueOnce(mergePerson); + mocks.person.reassignFaces.mockRejectedValue(new Error('update failed')); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); + + await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ + { id: mergePerson.id, success: false, error: BulkIdErrorReason.UNKNOWN }, ]); expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); describe('getStatistics', () => { it('should get correct number of person', async () => { - mocks.person.getById.mockResolvedValue(personStub.primaryPerson); - mocks.person.getStatistics.mockResolvedValue(statistics); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + mocks.person.getStatistics.mockResolvedValue({ assets: 3 }); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + await expect(sut.getStatistics(auth, person.id)).resolves.toEqual({ assets: 3 }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should require person.read permission', async () => { - mocks.person.getById.mockResolvedValue(personStub.primaryPerson); - await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + await expect(sut.getStatistics(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 090b358223..8a902590e3 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -595,7 +595,7 @@ export class PersonService extends BaseService { update.birthDate = mergePerson.birthDate; } - if (Object.keys(update).length > 0) { + if (Object.keys(update).length > 1) { primaryPerson = await this.personRepository.update(update); } diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 5f1125eaed..62575d0f07 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -5,7 +5,6 @@ import { SearchService } from 'src/services/search.service'; import { AssetFactory } from 'test/factories/asset.factory'; import { AuthFactory } from 'test/factories/auth.factory'; import { authStub } from 'test/fixtures/auth.stub'; -import { personStub } from 'test/fixtures/person.stub'; import { newTestService, ServiceMocks } from 'test/utils'; import { beforeEach, vitest } from 'vitest'; @@ -26,17 +25,18 @@ describe(SearchService.name, () => { describe('searchPerson', () => { it('should pass options to search', async () => { - const { name } = personStub.withName; + const auth = AuthFactory.create(); + const name = 'foo'; mocks.person.getByName.mockResolvedValue([]); - await sut.searchPerson(authStub.user1, { name, withHidden: false }); + await sut.searchPerson(auth, { name, withHidden: false }); - expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false }); + expect(mocks.person.getByName).toHaveBeenCalledWith(auth.user.id, name, { withHidden: false }); - await sut.searchPerson(authStub.user1, { name, withHidden: true }); + await sut.searchPerson(auth, { name, withHidden: true }); - expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true }); + expect(mocks.person.getByName).toHaveBeenCalledWith(auth.user.id, name, { withHidden: true }); }); }); diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index 9d48fcc8f8..6ab32e1f02 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -2,171 +2,6 @@ import { AssetFileType, AssetType } from 'src/enum'; import { AssetFileFactory } from 'test/factories/asset-file.factory'; import { userStub } from 'test/fixtures/user.stub'; -const updateId = '0d1173e3-4d80-4d76-b41e-57d56de21125'; - -export const personStub = { - noName: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - hidden: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: true, - isFavorite: false, - color: 'red', - }), - withName: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 1', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: 'assetFaceId', - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - withBirthDate: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 1', - birthDate: new Date('1976-06-30'), - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - noThumbnail: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - newThumbnail: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '/new/path/to/thumbnail.jpg', - faces: [], - faceAssetId: 'asset-id', - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - primaryPerson: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 1', - birthDate: null, - thumbnailPath: '/path/to/thumbnail', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - mergePerson: Object.freeze({ - id: 'person-2', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 2', - birthDate: null, - thumbnailPath: '/path/to/thumbnail', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - randomPerson: Object.freeze({ - id: 'person-3', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '/path/to/thumbnail', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - isFavorite: Object.freeze({ - id: 'person-4', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 1', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: 'assetFaceId', - faceAsset: null, - isHidden: false, - isFavorite: true, - color: 'red', - }), -}; - export const personThumbnailStub = { newThumbnailStart: Object.freeze({ ownerId: userStub.admin.id,