refactor: person stubs (#26512)

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