Files
Immich/server/test/medium/specs/services/person.service.spec.ts
T
d806bc002c fix: person without assets (#32117)
fix: person without assets (#32114)

(cherry picked from commit c0506fe723995808f4effd8d40d0235fcaf104b5)

Co-authored-by: Jason Rasmussen <jason@rasm.me>
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-10-06 08:38:24 +00:00

1536 lines
59 KiB
TypeScript

import { NotFoundException } from '@nestjs/common';
import { Kysely } from 'kysely';
import { DateTime } from 'luxon';
import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto.js';
import { AssetFaceCreateDto, PersonSearchDto, PersonUserRole } from 'src/dtos/person.dto.js';
import { AssetFileType, JobName } from 'src/enum.js';
import { AccessRepository } from 'src/repositories/access.repository.js';
import { AssetEditRepository } from 'src/repositories/asset-edit.repository.js';
import { AssetJobRepository } from 'src/repositories/asset-job.repository.js';
import { AssetRepository } from 'src/repositories/asset.repository.js';
import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository.js';
import { ConfigRepository } from 'src/repositories/config.repository.js';
import { DatabaseRepository } from 'src/repositories/database.repository.js';
import { JobRepository } from 'src/repositories/job.repository.js';
import { LoggingRepository } from 'src/repositories/logging.repository.js';
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository.js';
import { PartnerRepository } from 'src/repositories/partner.repository.js';
import { PersonUserRepository } from 'src/repositories/person-user.repository.js';
import { PersonRepository } from 'src/repositories/person.repository.js';
import { StorageRepository } from 'src/repositories/storage.repository.js';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository.js';
import { UserRepository } from 'src/repositories/user.repository.js';
import { DB } from 'src/schema/index.js';
import { PersonService } from 'src/services/person.service.js';
import { newMediumService } from 'test/medium.factory.js';
import { factory, newUuid } from 'test/small.factory.js';
import { getKyselyDB } from 'test/utils.js';
let defaultDatabase: Kysely<DB>;
const setup = (db?: Kysely<DB>) => {
return newMediumService(PersonService, {
database: db || defaultDatabase,
real: [
AccessRepository,
AssetJobRepository,
ConfigRepository,
DatabaseRepository,
PersonRepository,
PersonUserRepository,
AssetRepository,
AssetEditRepository,
SystemMetadataRepository,
UserRepository,
ClusterGroupRepository,
PartnerRepository,
],
mock: [JobRepository, LoggingRepository, StorageRepository, MachineLearningRepository],
});
};
beforeAll(async () => {
defaultDatabase = await getKyselyDB();
});
describe(PersonService.name, () => {
describe('reassignFaces', () => {
it('should require access to the person the auth user is assigning to', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: user2 } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user2.id });
await expect(sut.reassignFaces(factory.auth({ user }), person.personGroupId, { data: [] })).rejects.toThrow(
'Not found or no person.update access',
);
});
it('should allow person owner access', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
await expect(sut.reassignFaces(factory.auth({ user }), person.personGroupId, { data: [] })).resolves.toEqual([]);
});
it('should not allow shared read only access', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: user2 } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user2.id });
await ctx.newPersonUser({
personGroupId: person.personGroupId,
sharedById: user2.id,
sharedWithId: user.id,
role: PersonUserRole.Read,
});
await expect(
sut.reassignFaces(factory.auth({ user }), person.personGroupId, {
data: [{ personId: person.personGroupId, userId: user2.id, assetId: newUuid() }],
}),
).rejects.toThrow('Not found or no person.update access');
});
it('should allow shared write access', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: user2 } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user2.id });
await ctx.newPersonUser({
personGroupId: person.personGroupId,
sharedById: user2.id,
sharedWithId: user.id,
role: PersonUserRole.Write,
});
await expect(
sut.reassignFaces(factory.auth({ user }), person.personGroupId, {
data: [{ personId: person.personGroupId, userId: user2.id, assetId: newUuid() }],
}),
).resolves.toEqual([expect.objectContaining({ id: person.personGroupId })]);
});
});
describe('getAll', () => {
it('should filter by sharing direction and user', async () => {
const { ctx, sut } = setup(await getKyselyDB());
const { user: user1 } = await ctx.newUser();
const { user: user2 } = await ctx.newUser({ clusterGroupId: user1.clusterGroupId });
const { user: user3 } = await ctx.newUser({ clusterGroupId: user1.clusterGroupId });
const { asset } = await ctx.newAsset({ ownerId: user1.id });
const { person: sharedByMe } = await ctx.newPerson({ ownerId: user1.id, name: 'Shared by me' });
const { person: notShared } = await ctx.newPerson({ ownerId: user1.id, name: 'Not shared' });
const { person: sharedWithMe } = await ctx.newPerson({ ownerId: user3.id, name: 'Shared with me' });
await ctx.newAssetFace({ assetId: asset.id, personGroupId: sharedByMe.personGroupId });
await ctx.newAssetFace({ assetId: asset.id, personGroupId: notShared.personGroupId });
await ctx.newAssetFace({ assetId: asset.id, personGroupId: sharedWithMe.personGroupId });
await ctx.newPersonUser({
personGroupId: sharedByMe.personGroupId,
sharedById: user1.id,
sharedWithId: user2.id,
});
await ctx.newPersonUser({
personGroupId: sharedWithMe.personGroupId,
sharedById: user3.id,
sharedWithId: user1.id,
});
const auth = factory.auth({ user: user1 });
const getIds = async (dto: Partial<PersonSearchDto>) => {
const { people, total } = await sut.getAll(auth, { page: 1, size: 10, ...dto });
expect(total).toBe(people.length);
return people.map(({ id }) => id).toSorted();
};
await expect(getIds({})).resolves.toEqual(
[sharedByMe.personGroupId, notShared.personGroupId, sharedWithMe.personGroupId].toSorted(),
);
await expect(getIds({ sharedById: user1.id })).resolves.toEqual([sharedByMe.personGroupId]);
await expect(getIds({ sharedWithId: user2.id })).resolves.toEqual([sharedByMe.personGroupId]);
await expect(getIds({ sharedWithId: user3.id })).resolves.toEqual([]);
await expect(getIds({ sharedWithId: user1.id })).resolves.toEqual([sharedWithMe.personGroupId]);
await expect(getIds({ sharedById: user3.id })).resolves.toEqual([sharedWithMe.personGroupId]);
await expect(getIds({ sharedById: user2.id })).resolves.toEqual([]);
});
it('should filter by favorite and hidden', async () => {
const { ctx, sut } = setup(await getKyselyDB());
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
const { person: favorite } = await ctx.newPerson({ ownerId: user.id, isFavorite: true });
const { person: hidden } = await ctx.newPerson({ ownerId: user.id, isHidden: true });
const { person: neither } = await ctx.newPerson({ ownerId: user.id });
for (const person of [favorite, hidden, neither]) {
await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId });
}
const auth = factory.auth({ user });
const getIds = async (dto: Partial<PersonSearchDto>) => {
const { people, total } = await sut.getAll(auth, { page: 1, size: 10, withHidden: true, ...dto });
expect(total).toBe(people.length);
return people.map(({ id }) => id).toSorted();
};
await expect(getIds({ isFavorite: true })).resolves.toEqual([favorite.personGroupId]);
await expect(getIds({ isFavorite: false })).resolves.toEqual(
[hidden.personGroupId, neither.personGroupId].toSorted(),
);
await expect(getIds({ isHidden: true })).resolves.toEqual([hidden.personGroupId]);
await expect(getIds({ isHidden: false })).resolves.toEqual(
[favorite.personGroupId, neither.personGroupId].toSorted(),
);
await expect(getIds({ isFavorite: true, isHidden: true })).resolves.toEqual([]);
});
it('should include hidden people when filtering by isHidden without withHidden', async () => {
const { ctx, sut } = setup(await getKyselyDB());
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
const { person: hidden } = await ctx.newPerson({ ownerId: user.id, isHidden: true });
const { person: visible } = await ctx.newPerson({ ownerId: user.id });
await ctx.newAssetFace({ assetId: asset.id, personGroupId: hidden.personGroupId });
await ctx.newAssetFace({ assetId: asset.id, personGroupId: visible.personGroupId });
const auth = factory.auth({ user });
await expect(sut.getAll(auth, { page: 1, size: 10, isHidden: true })).resolves.toEqual(
expect.objectContaining({ people: [expect.objectContaining({ id: hidden.personGroupId })], total: 1 }),
);
await expect(sut.getAll(auth, { page: 1, size: 10, isHidden: false })).resolves.toEqual(
expect.objectContaining({ people: [expect.objectContaining({ id: visible.personGroupId })], total: 1 }),
);
});
});
describe('getById', () => {
it('should require person.read access', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
await expect(sut.getById(factory.auth(), person.personGroupId)).rejects.toThrow(
'Not found or no person.read access',
);
});
it('should return own version of shared person but copy over a shared name and birth date', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: user2 } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user2.id, name: "User2's person", birthDate: new Date() });
await ctx.newPersonUser({ personGroupId: person.personGroupId, sharedById: user2.id, sharedWithId: user.id });
await expect(sut.getById(factory.auth({ user }), person.personGroupId)).resolves.toEqual(
expect.objectContaining({
id: person.personGroupId,
name: person.name,
birthDate: expect.any(String),
otherPeople: [expect.objectContaining({ sharedById: user2.id })],
}),
);
});
it('should include the users the person is shared by and shared with', async () => {
const { ctx, sut } = setup();
const { user: owner } = await ctx.newUser();
const { user: user1 } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { person } = await ctx.newPerson({ ownerId: owner.id });
await ctx.newPersonUser({
personGroupId: person.personGroupId,
sharedById: owner.id,
sharedWithId: user1.id,
role: PersonUserRole.Write,
});
await expect(sut.getById(factory.auth({ user: owner }), person.personGroupId)).resolves.toEqual(
expect.objectContaining({
sharedBy: [],
sharedWith: [expect.objectContaining({ id: user1.id, email: user1.email, role: PersonUserRole.Write })],
}),
);
await expect(sut.getById(factory.auth({ user: user1 }), person.personGroupId)).resolves.toEqual(
expect.objectContaining({
sharedBy: [expect.objectContaining({ id: owner.id, email: owner.email, role: PersonUserRole.Write })],
sharedWith: [],
}),
);
});
});
describe('getThumbnail', () => {
it('should require person.read access', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
await expect(sut.getThumbnail(factory.auth(), person.personGroupId)).rejects.toThrow(
'Not found or no person.read access',
);
});
it('should return own thumbnail path', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
await expect(sut.getThumbnail(factory.auth({ user }), person.personGroupId)).resolves.toEqual(
expect.objectContaining({ path: person.thumbnailPath }),
);
});
it('should fall back to shared thumbnail path', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: user2 } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user2.id });
await ctx.newPersonUser({ personGroupId: person.personGroupId, sharedById: user2.id, sharedWithId: user.id });
await expect(sut.getThumbnail(factory.auth({ user }), person.personGroupId)).resolves.toEqual(
expect.objectContaining({ path: person.thumbnailPath }),
);
});
it('should fail if there is no (shared) thumbnail available', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: user2 } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id, thumbnailPath: undefined });
await ctx.newPerson({ ownerId: user2.id, personGroupId: person.personGroupId });
await expect(sut.getThumbnail(factory.auth({ user }), person.personGroupId)).rejects.toBeInstanceOf(
NotFoundException,
);
});
});
describe('update', () => {
it('should throw an error when there is no access', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: user2 } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user2.id });
await expect(sut.update(factory.auth({ user }), person.personGroupId, { name: 'New name' })).rejects.toThrow(
'Not found or no person.update access',
);
});
it('should update the name and birth date for every user with write access', async () => {
const { ctx, sut } = setup();
const personRepo = ctx.get(PersonRepository);
const { user } = await ctx.newUser();
const { user: writer } = await ctx.newUser();
const { user: reader } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: writer.id, name: 'Old name' });
const { personGroupId } = person;
await ctx.newPerson({ ownerId: reader.id, personGroupId, name: 'Old name' });
await ctx.newPersonUser({
personGroupId,
sharedById: writer.id,
sharedWithId: user.id,
role: PersonUserRole.Write,
});
await ctx.newPersonUser({
personGroupId,
sharedById: reader.id,
sharedWithId: user.id,
role: PersonUserRole.Read,
});
await expect(
sut.update(factory.auth({ user }), personGroupId, { name: 'New name', birthDate: '2000-01-01' }),
).resolves.toEqual(expect.objectContaining({ name: 'New name', birthDate: '2000-01-01' }));
await expect(personRepo.getForUser({ userId: user.id, personGroupId })).resolves.toEqual(
expect.objectContaining({ name: 'New name', birthDate: '2000-01-01' }),
);
await expect(personRepo.getForUser({ userId: writer.id, personGroupId })).resolves.toEqual(
expect.objectContaining({ name: 'New name', birthDate: '2000-01-01' }),
);
await expect(personRepo.getForUser({ userId: reader.id, personGroupId })).resolves.toEqual(
expect.objectContaining({ name: 'Old name', birthDate: null }),
);
});
it('should only update the specified user when userId is provided', async () => {
const { ctx, sut } = setup();
const personRepo = ctx.get(PersonRepository);
const { user } = await ctx.newUser();
const { user: writer } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: writer.id, name: 'Old name' });
const { personGroupId } = person;
await ctx.newPersonUser({
personGroupId,
sharedById: writer.id,
sharedWithId: user.id,
role: PersonUserRole.Write,
});
await expect(
sut.update(factory.auth({ user }), personGroupId, { name: 'New name', userId: writer.id }),
).resolves.toEqual(expect.objectContaining({ name: 'New name' }));
await expect(personRepo.getForUser({ userId: writer.id, personGroupId })).resolves.toEqual(
expect.objectContaining({ name: 'New name' }),
);
await expect(personRepo.getForUser({ userId: user.id, personGroupId })).resolves.toEqual(
expect.objectContaining({ name: 'Old name' }),
);
});
it('should only update personal properties for the current user', async () => {
const { ctx, sut } = setup();
const personRepo = ctx.get(PersonRepository);
const { user } = await ctx.newUser();
const { user: writer } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: writer.id, name: 'Old name' });
const { personGroupId } = person;
await ctx.newPersonUser({
personGroupId,
sharedById: writer.id,
sharedWithId: user.id,
role: PersonUserRole.Write,
});
await expect(
sut.update(factory.auth({ user }), personGroupId, { name: 'New name', isFavorite: true, isHidden: true }),
).resolves.toEqual(expect.objectContaining({ name: 'New name', isFavorite: true, isHidden: true }));
await expect(personRepo.getForUser({ userId: writer.id, personGroupId })).resolves.toEqual(
expect.objectContaining({ name: 'New name', isFavorite: false, isHidden: false }),
);
});
it('should not allow personal properties to be updated for another user', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { user: writer } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: writer.id });
const { personGroupId } = person;
await ctx.newPersonUser({
personGroupId,
sharedById: writer.id,
sharedWithId: user.id,
role: PersonUserRole.Write,
});
await expect(
sut.update(factory.auth({ user }), personGroupId, { isFavorite: true, userId: writer.id }),
).rejects.toThrow('Only name and birthDate can be updated for other users');
});
});
describe('delete', () => {
it('should throw an error when there is no access', async () => {
const { sut } = setup();
const auth = factory.auth();
const personId = factory.uuid();
await expect(sut.delete(auth, personId, {})).rejects.toThrow('Not found or no person.delete access');
});
it('should delete the person', async () => {
const { sut, ctx } = setup();
const personRepo = ctx.get(PersonRepository);
const storageMock = ctx.getMock(StorageRepository);
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const auth = factory.auth({ user });
storageMock.unlink.mockResolvedValue();
await expect(personRepo.getByGroupId(person)).resolves.toEqual(
expect.objectContaining({ personGroupId: person.personGroupId }),
);
await expect(sut.delete(auth, person.personGroupId, {})).resolves.toBeUndefined();
await expect(personRepo.getByGroupId(person)).resolves.toBeUndefined();
expect(storageMock.unlink).toHaveBeenCalledWith(person.thumbnailPath);
});
});
describe('deleteAll', () => {
it('should throw an error when there is no access', async () => {
const { sut } = setup();
const auth = factory.auth();
const personId = factory.uuid();
await expect(sut.deleteAll(auth, { ids: [personId] })).rejects.toThrow('Not found or no person.delete access');
});
it('should delete the person', async () => {
const { sut, ctx } = setup();
const storageMock = ctx.getMock(StorageRepository);
const personRepo = ctx.get(PersonRepository);
const { user } = await ctx.newUser();
const { person: person1 } = await ctx.newPerson({ ownerId: user.id });
const { person: person2 } = await ctx.newPerson({ ownerId: user.id });
const auth = factory.auth({ user });
storageMock.unlink.mockResolvedValue();
await expect(
sut.deleteAll(auth, { ids: [person1.personGroupId, person2.personGroupId] }),
).resolves.toBeUndefined();
await expect(personRepo.getByGroupId(person1)).resolves.toBeUndefined();
await expect(personRepo.getByGroupId(person2)).resolves.toBeUndefined();
expect(storageMock.unlink).toHaveBeenCalledTimes(2);
expect(storageMock.unlink).toHaveBeenCalledWith(person1.thumbnailPath);
expect(storageMock.unlink).toHaveBeenCalledWith(person2.thumbnailPath);
});
});
describe('handleDetectFaces', () => {
it('should prefer an edited preview file', async () => {
const { sut, ctx } = setup();
const config = await ctx.getConfig();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, description: '' });
await ctx.newAssetFile({
assetId: asset.id,
type: AssetFileType.Preview,
isEdited: true,
path: 'edited_file.jpg',
});
await ctx.newAssetFile({
assetId: asset.id,
type: AssetFileType.Preview,
isEdited: false,
path: 'unedited_file.jpg',
});
ctx
.getMock(MachineLearningRepository)
.detectFaces.mockResolvedValue({ imageHeight: 42, imageWidth: 69, faces: [] });
await sut.handleDetectFaces({ id: asset.id });
expect(ctx.getMock(MachineLearningRepository).detectFaces).toHaveBeenCalledWith(
'edited_file.jpg',
config.machineLearning.facialRecognition,
);
});
});
describe('handleQueueRecognizeFaces', () => {
it('should delete all people and queue faces for recognition', async () => {
const { sut, ctx } = setup();
const jobRepo = ctx.getMock(JobRepository);
ctx.getMock(StorageRepository).unlink.mockResolvedValue();
jobRepo.waitForQueueCompletion.mockResolvedValue();
jobRepo.getJobCounts.mockResolvedValue({ active: 0, waiting: 0, completed: 0, delayed: 0, failed: 0, paused: 0 });
jobRepo.queueAll.mockResolvedValue();
const { user } = await ctx.newUser();
const { user: user1 } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
const { asset: assetUser1 } = await ctx.newAsset({ ownerId: user1.id });
const { person } = await ctx.newPerson({ ownerId: user.id });
const { person: personUser1 } = await ctx.newPerson({ ownerId: user1.id });
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId });
const { assetFace: assetFaceUser1 } = await ctx.newAssetFace({
assetId: assetUser1.id,
personGroupId: personUser1.personGroupId,
});
await sut.handleQueueRecognizeFaces({ force: true });
await expect(ctx.database.selectFrom('person').selectAll().execute()).resolves.toHaveLength(0);
expect(jobRepo.queueAll).toHaveBeenCalledWith(
expect.objectContaining([
{ name: JobName.FacialRecognition, data: { id: assetFace.id, deferred: false } },
{ name: JobName.FacialRecognition, data: { id: assetFaceUser1.id, deferred: false } },
]),
);
});
it('should only delete all people of a specified cluster group and queue their faces for recognition', async () => {
const { sut, ctx } = setup();
const jobRepo = ctx.getMock(JobRepository);
ctx.getMock(StorageRepository).unlink.mockResolvedValue();
jobRepo.waitForQueueCompletion.mockResolvedValue();
jobRepo.getJobCounts.mockResolvedValue({ active: 0, waiting: 0, completed: 0, delayed: 0, failed: 0, paused: 0 });
jobRepo.queueAll.mockResolvedValue();
const { user } = await ctx.newUser();
const { user: user1 } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
const { asset: assetUser1 } = await ctx.newAsset({ ownerId: user1.id });
const { person } = await ctx.newPerson({ ownerId: user.id });
const { person: personUser1 } = await ctx.newPerson({ ownerId: user1.id });
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId });
const { assetFace: assetFaceUser1 } = await ctx.newAssetFace({
assetId: assetUser1.id,
personGroupId: personUser1.personGroupId,
});
await sut.handleQueueRecognizeFaces({ force: true, clusterGroupId: user.clusterGroupId });
await expect(ctx.database.selectFrom('person').selectAll().execute()).resolves.toHaveLength(1);
expect(jobRepo.queueAll).toHaveBeenCalledWith(
expect.objectContaining([{ name: JobName.FacialRecognition, data: { id: assetFace.id, deferred: false } }]),
);
expect(jobRepo.queueAll).not.toHaveBeenCalledWith(
expect.objectContaining([
{ name: JobName.FacialRecognition, data: { id: assetFace.id, deferred: false } },
{ name: JobName.FacialRecognition, data: { id: assetFaceUser1.id, deferred: false } },
]),
);
});
});
describe('mergePeople', () => {
it('should merge people of multiple users', async () => {
const { sut, ctx } = setup();
const storageMock = ctx.getMock(StorageRepository);
const { user: user1 } = await ctx.newUser();
const { user: user2 } = await ctx.newUser({ clusterGroupId: user1.clusterGroupId });
const { person: person1 } = await ctx.newPerson({ ownerId: user1.id, name: undefined });
const { person: person2 } = await ctx.newPerson({ ownerId: user1.id });
await ctx.newPerson({
ownerId: user2.id,
personGroupId: person1.personGroupId,
});
await ctx.newPerson({
ownerId: user2.id,
personGroupId: person2.personGroupId,
name: undefined,
});
const { asset } = await ctx.newAsset({ ownerId: user2.id });
await ctx.newAssetFace({ assetId: asset.id, personGroupId: person2.personGroupId });
storageMock.unlink.mockResolvedValue();
const auth = factory.auth({ user: user1 });
await sut.mergePeople(auth, { ids: [person1.personGroupId, person2.personGroupId] });
const user1People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user1.id }));
const user2People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user2.id }));
expect(user1People).toEqual([expect.objectContaining({ personGroupId: person1.personGroupId })]);
expect(user2People).toEqual([expect.objectContaining({ personGroupId: person1.personGroupId })]);
await expect(ctx.get(PersonRepository).getFaces(asset.id, { viewingUserId: asset.ownerId })).resolves.toEqual([
expect.objectContaining({ personGroupId: person1.personGroupId }),
]);
});
it('should skip people with a different name', async () => {
const { sut, ctx } = setup();
const storageMock = ctx.getMock(StorageRepository);
const { user: user1 } = await ctx.newUser();
const { user: user2 } = await ctx.newUser({ clusterGroupId: user1.clusterGroupId });
const { person: person1 } = await ctx.newPerson({ ownerId: user1.id });
const { person: person2 } = await ctx.newPerson({ ownerId: user1.id });
await ctx.newPerson({
ownerId: user2.id,
personGroupId: person1.personGroupId,
name: 'Person 1',
});
await ctx.newPerson({
ownerId: user2.id,
personGroupId: person2.personGroupId,
name: 'Person 2',
});
storageMock.unlink.mockResolvedValue();
const auth = factory.auth({ user: user1 });
await sut.mergePeople(auth, { ids: [person1.personGroupId, person2.personGroupId] });
const user1People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user1.id }));
const user2People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user2.id }));
expect(user1People).toEqual([expect.objectContaining({ personGroupId: person1.personGroupId })]);
expect(user2People).toEqual(
expect.arrayContaining([
expect.objectContaining({ personGroupId: person1.personGroupId }),
expect.objectContaining({ personGroupId: person2.personGroupId }),
]),
);
});
it('should skip people with a different birth date', async () => {
const { sut, ctx } = setup();
const storageMock = ctx.getMock(StorageRepository);
const { user: user1 } = await ctx.newUser();
const { user: user2 } = await ctx.newUser({ clusterGroupId: user1.clusterGroupId });
const { person: person1 } = await ctx.newPerson({ ownerId: user1.id });
const { person: person2 } = await ctx.newPerson({ ownerId: user1.id });
await ctx.newPerson({
ownerId: user2.id,
personGroupId: person1.personGroupId,
birthDate: DateTime.now().minus({ years: 1 }).toJSDate(),
});
await ctx.newPerson({
ownerId: user2.id,
personGroupId: person2.personGroupId,
birthDate: DateTime.now().minus({ years: 2 }).toJSDate(),
});
storageMock.unlink.mockResolvedValue();
const auth = factory.auth({ user: user1 });
await sut.mergePeople(auth, { ids: [person1.personGroupId, person2.personGroupId] });
const user1People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user1.id }));
const user2People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user2.id }));
expect(user1People).toEqual([expect.objectContaining({ personGroupId: person1.personGroupId })]);
expect(user2People).toEqual(
expect.arrayContaining([
expect.objectContaining({ personGroupId: person1.personGroupId }),
expect.objectContaining({ personGroupId: person2.personGroupId }),
]),
);
});
});
describe('createFace', () => {
it('should store and retrieve the face as-is when there are no edits', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 200 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 200,
imageHeight: 200,
x: 50,
y: 50,
width: 150,
height: 150,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
// retrieve an asset's faces
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 50,
boundingBoxY1: 50,
boundingBoxX2: 200,
boundingBoxY2: 200,
}),
]),
);
});
it('should properly transform the coordinates when the asset is edited (Crop)', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 200 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Crop,
parameters: {
x: 50,
y: 50,
width: 150,
height: 200,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 150,
imageHeight: 200,
x: 0,
y: 0,
width: 100,
height: 100,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
// retrieve an asset's faces
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 0,
boundingBoxY1: 0,
boundingBoxX2: 100,
boundingBoxY2: 100,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toHaveLength(1);
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 50,
boundingBoxY1: 50,
boundingBoxX2: 150,
boundingBoxY2: 150,
}),
]),
);
});
it('should properly transform the coordinates when the asset is edited (Rotate 90)', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 200 });
await ctx.newExif({ assetId: asset.id, exifImageWidth: 200, exifImageHeight: 100 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Rotate,
parameters: {
angle: 90,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 100,
imageHeight: 200,
x: 25,
y: 50,
width: 10,
height: 10,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: expect.closeTo(25, 1),
boundingBoxY1: expect.closeTo(50, 1),
boundingBoxX2: expect.closeTo(35, 1),
boundingBoxY2: expect.closeTo(60, 1),
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 50,
boundingBoxY1: 65,
boundingBoxX2: 60,
boundingBoxY2: 75,
}),
]),
);
});
it('should properly transform the coordinates when the asset is edited (Mirror Horizontal)', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 100 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 200 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Mirror,
parameters: {
axis: MirrorAxis.Horizontal,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 200,
imageHeight: 100,
x: 50,
y: 25,
width: 100,
height: 50,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 50,
boundingBoxY1: 25,
boundingBoxX2: 150,
boundingBoxY2: 75,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 50,
boundingBoxY1: 25,
boundingBoxX2: 150,
boundingBoxY2: 75,
}),
]),
);
});
it('should properly transform the coordinates when the asset is edited (Crop + Rotate)', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 150 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Crop,
parameters: {
x: 50,
y: 0,
width: 150,
height: 200,
},
},
{
action: AssetEditAction.Rotate,
parameters: {
angle: 90,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 200,
imageHeight: 150,
x: 50,
y: 25,
width: 10,
height: 20,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: expect.closeTo(50, 1),
boundingBoxY1: expect.closeTo(25, 1),
boundingBoxX2: expect.closeTo(60, 1),
boundingBoxY2: expect.closeTo(45, 1),
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 75,
boundingBoxY1: 140,
boundingBoxX2: 95,
boundingBoxY2: 150,
}),
]),
);
});
it('should properly transform the coordinates when the asset is edited (Crop + Mirror)', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 100 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 200 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Crop,
parameters: {
x: 50,
y: 0,
width: 150,
height: 100,
},
},
{
action: AssetEditAction.Mirror,
parameters: {
axis: MirrorAxis.Horizontal,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 150,
imageHeight: 100,
x: 25,
y: 25,
width: 75,
height: 50,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 25,
boundingBoxY1: 25,
boundingBoxX2: 100,
boundingBoxY2: 75,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 100,
boundingBoxY1: 25,
boundingBoxX2: 175,
boundingBoxY2: 75,
}),
]),
);
});
it('should properly transform the coordinates when the asset is edited (Rotate + Mirror)', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 150 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 150 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Rotate,
parameters: {
angle: 90,
},
},
{
action: AssetEditAction.Mirror,
parameters: {
axis: MirrorAxis.Horizontal,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 200,
imageHeight: 150,
x: 50,
y: 25,
width: 15,
height: 20,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: expect.closeTo(50, 1),
boundingBoxY1: expect.closeTo(25, 1),
boundingBoxX2: expect.closeTo(65, 1),
boundingBoxY2: expect.closeTo(45, 1),
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 25,
boundingBoxY1: 50,
boundingBoxX2: 45,
boundingBoxY2: 65,
}),
]),
);
});
it('should properly transform the coordinates when the asset is edited (Crop + Rotate + Mirror)', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 100 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Crop,
parameters: {
x: 50,
y: 25,
width: 100,
height: 150,
},
},
{
action: AssetEditAction.Rotate,
parameters: {
angle: 270,
},
},
{
action: AssetEditAction.Mirror,
parameters: {
axis: MirrorAxis.Horizontal,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 150,
imageHeight: 150,
x: 25,
y: 50,
width: 75,
height: 50,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 25,
boundingBoxY1: 49,
boundingBoxX2: 99,
boundingBoxY2: 100,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 50,
boundingBoxY1: 75,
boundingBoxX2: 100,
boundingBoxY2: 150,
}),
]),
);
});
it('should properly transform the coordinates with multiple mirrors in sequence', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 100 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 100 });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Mirror,
parameters: {
axis: MirrorAxis.Horizontal,
},
},
{
action: AssetEditAction.Mirror,
parameters: {
axis: MirrorAxis.Vertical,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 100,
imageHeight: 100,
x: 10,
y: 10,
width: 80,
height: 80,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 10,
boundingBoxY1: 10,
boundingBoxX2: 90,
boundingBoxY2: 90,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 10,
boundingBoxY1: 10,
boundingBoxX2: 90,
boundingBoxY2: 90,
}),
]),
);
});
it('should properly handle exif orientation when creating a face on an edited asset', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 100 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 100, orientation: '6' });
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Mirror,
parameters: {
axis: MirrorAxis.Horizontal,
},
},
{
action: AssetEditAction.Mirror,
parameters: {
axis: MirrorAxis.Vertical,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 100,
imageHeight: 100,
x: 10,
y: 10,
width: 80,
height: 80,
personId: person.personGroupId,
assetId: asset.id,
};
await sut.createFace(auth, dto);
const faces = sut.getFacesById(auth, { id: asset.id });
await expect(faces).resolves.toHaveLength(1);
await expect(faces).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 110,
boundingBoxY1: 10,
boundingBoxX2: 190,
boundingBoxY2: 90,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.newEdits(asset.id, { edits: [] });
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.personGroupId }),
boundingBoxX1: 10,
boundingBoxY1: 10,
boundingBoxX2: 90,
boundingBoxY2: 90,
}),
]),
);
});
});
describe('upsertPeopleUsers', () => {
it('should throw error for sharedWith users that are not in the same cluster group', async () => {
const { sut, ctx } = setup();
const { user: owner } = await ctx.newUser();
const { user: user1 } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: owner.id });
const auth = factory.auth({ user: owner });
await expect(
sut.upsertPeopleUsers(auth, {
personIds: [person.personGroupId],
sharedWithIds: [user1.id],
role: PersonUserRole.Read,
}),
).rejects.toThrow('All users must be in the same cluster group');
await expect(sut.getUsersForPeople(auth, {})).resolves.toHaveLength(0);
});
it('should add user to person', async () => {
const { sut, ctx } = setup();
const { user: owner } = await ctx.newUser();
const { user: user1 } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { person } = await ctx.newPerson({ ownerId: owner.id });
const auth = factory.auth({ user: owner });
await sut.upsertPeopleUsers(auth, {
personIds: [person.personGroupId],
sharedWithIds: [user1.id],
role: PersonUserRole.Read,
});
await expect(sut.getUsersForPeople(auth, {})).resolves.toEqual([
expect.objectContaining({
sharedBy: expect.objectContaining({ id: owner.id }),
sharedWith: expect.objectContaining({ id: user1.id }),
}),
]);
});
it('should update the name and birthdate when sharing to an existing copy', async () => {
const { sut, ctx } = setup();
const { user: owner } = await ctx.newUser();
const { user: user1 } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { person } = await ctx.newPerson({ ownerId: owner.id, name: 'Owner name', birthDate: '1990-01-01' });
await ctx.newPerson({ ownerId: user1.id, personGroupId: person.personGroupId, name: '', birthDate: null });
await sut.upsertPeopleUsers(factory.auth({ user: owner }), {
personIds: [person.personGroupId],
sharedWithIds: [user1.id],
role: PersonUserRole.Read,
});
await expect(sut.getById(factory.auth({ user: user1 }), person.personGroupId)).resolves.toEqual(
expect.objectContaining({ name: 'Owner name', birthDate: '1990-01-01' }),
);
});
it('should skip updating when sharing to an existing copy with values', async () => {
const { sut, ctx } = setup();
const { user: owner } = await ctx.newUser();
const { user: user1 } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { person } = await ctx.newPerson({ ownerId: owner.id, name: 'Owner name', birthDate: '1990-01-01' });
await ctx.newPerson({
ownerId: user1.id,
personGroupId: person.personGroupId,
name: 'User1 name',
birthDate: '2000-02-02',
});
await sut.upsertPeopleUsers(factory.auth({ user: owner }), {
personIds: [person.personGroupId],
sharedWithIds: [user1.id],
role: PersonUserRole.Read,
});
await expect(sut.getById(factory.auth({ user: user1 }), person.personGroupId)).resolves.toEqual(
expect.objectContaining({ name: 'User1 name', birthDate: '2000-02-02' }),
);
});
});
describe('removeUsersFromPeople', () => {
it('should work with an empty list', async () => {
const { sut, ctx } = setup();
const { user: owner } = await ctx.newUser();
const { user: sharedWith } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { person } = await ctx.newPerson({ ownerId: owner.id });
const auth = factory.auth({ user: owner });
await sut.upsertPeopleUsers(auth, {
personIds: [person.personGroupId],
sharedWithIds: [sharedWith.id],
role: PersonUserRole.Read,
});
await sut.removeUsersFromPeople(auth, []);
await expect(sut.getUsersForPeople(auth, {})).resolves.toHaveLength(1);
});
it('should throw an error when there is no access', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { user: owner } = await ctx.newUser({ clusterGroupId: user.clusterGroupId });
const { person } = await ctx.newPerson({ ownerId: owner.id });
const auth = factory.auth({ user });
await expect(
sut.removeUsersFromPeople(auth, [{ personId: person.personGroupId, sharedWithId: user.id }]),
).rejects.toThrow('Not found or no person.update access');
});
it('should delete only the requested person and user pairs', async () => {
const { sut, ctx } = setup();
const { user: owner } = await ctx.newUser();
const { user: user1 } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { user: user2 } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { person: person1 } = await ctx.newPerson({ ownerId: owner.id });
const { person: person2 } = await ctx.newPerson({ ownerId: owner.id });
const auth = factory.auth({ user: owner });
await sut.upsertPeopleUsers(auth, {
personIds: [person1.personGroupId, person2.personGroupId],
sharedWithIds: [user1.id, user2.id],
role: PersonUserRole.Read,
});
await expect(sut.getUsersForPeople(auth, {})).resolves.toHaveLength(4);
await sut.removeUsersFromPeople(auth, [
{ personId: person1.personGroupId, sharedWithId: user1.id },
{ personId: person2.personGroupId, sharedWithId: user2.id },
]);
const shares = await sut.getUsersForPeople(auth, {});
expect(shares).toHaveLength(2);
expect(shares).toEqual(
expect.arrayContaining([
expect.objectContaining({ personId: person1.personGroupId, sharedWithId: user2.id }),
expect.objectContaining({ personId: person2.personGroupId, sharedWithId: user1.id }),
]),
);
});
it('should not delete the same pair shared by another user', async () => {
const { sut, ctx } = setup();
const { user: owner } = await ctx.newUser();
const { user: otherOwner } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { user: sharedWith } = await ctx.newUser({ clusterGroupId: owner.clusterGroupId });
const { person } = await ctx.newPerson({ ownerId: owner.id });
await ctx.newPerson({ ownerId: otherOwner.id, personGroupId: person.personGroupId });
const auth = factory.auth({ user: owner });
const otherAuth = factory.auth({ user: otherOwner });
const dto = { personIds: [person.personGroupId], sharedWithIds: [sharedWith.id], role: PersonUserRole.Read };
await sut.upsertPeopleUsers(auth, dto);
await sut.upsertPeopleUsers(otherAuth, dto);
await sut.removeUsersFromPeople(auth, [{ personId: person.personGroupId, sharedWithId: sharedWith.id }]);
await expect(sut.getUsersForPeople(auth, {})).resolves.toEqual([]);
await expect(sut.getUsersForPeople(otherAuth, {})).resolves.toEqual([
expect.objectContaining({ personId: person.personGroupId, sharedWithId: sharedWith.id }),
]);
});
});
});