From eca9b5684751b09cbad46a200b9fdd9a206193e6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 25 Jun 2025 11:12:36 -0400 Subject: [PATCH] feat(server): person delete (#19511) feat(api): person delete --- mobile/openapi/README.md | 2 + mobile/openapi/lib/api/people_api.dart | 79 ++++++++++++++++ open-api/immich-openapi-specs.json | 66 ++++++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 17 ++++ .../src/controllers/person.controller.spec.ts | 42 +++++++++ server/src/controllers/person.controller.ts | 31 ++++++- server/src/queries/person.repository.sql | 9 ++ server/src/repositories/person.repository.ts | 12 ++- server/src/services/person.service.ts | 18 +++- server/src/utils/access.ts | 19 ++-- server/test/medium.factory.ts | 14 ++- .../specs/services/person.service.spec.ts | 90 +++++++++++++++++++ .../repositories/person.repository.mock.ts | 38 -------- server/test/utils.ts | 3 +- 14 files changed, 380 insertions(+), 60 deletions(-) create mode 100644 server/test/medium/specs/services/person.service.spec.ts delete mode 100644 server/test/repositories/person.repository.mock.ts diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index a412c237dd..590e45f04b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -169,6 +169,8 @@ Class | Method | HTTP request | Description *PartnersApi* | [**removePartner**](doc//PartnersApi.md#removepartner) | **DELETE** /partners/{id} | *PartnersApi* | [**updatePartner**](doc//PartnersApi.md#updatepartner) | **PUT** /partners/{id} | *PeopleApi* | [**createPerson**](doc//PeopleApi.md#createperson) | **POST** /people | +*PeopleApi* | [**deletePeople**](doc//PeopleApi.md#deletepeople) | **DELETE** /people | +*PeopleApi* | [**deletePerson**](doc//PeopleApi.md#deleteperson) | **DELETE** /people/{id} | *PeopleApi* | [**getAllPeople**](doc//PeopleApi.md#getallpeople) | **GET** /people | *PeopleApi* | [**getPerson**](doc//PeopleApi.md#getperson) | **GET** /people/{id} | *PeopleApi* | [**getPersonStatistics**](doc//PeopleApi.md#getpersonstatistics) | **GET** /people/{id}/statistics | diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 1cdb878852..35dbac4e97 100644 --- a/mobile/openapi/lib/api/people_api.dart +++ b/mobile/openapi/lib/api/people_api.dart @@ -63,6 +63,85 @@ class PeopleApi { return null; } + /// Performs an HTTP 'DELETE /people' operation and returns the [Response]. + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future deletePeopleWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/people'; + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future deletePeople(BulkIdsDto bulkIdsDto,) async { + final response = await deletePeopleWithHttpInfo(bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'DELETE /people/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future deletePersonWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/people/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future deletePerson(String id,) async { + final response = await deletePersonWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'GET /people' operation and returns the [Response]. /// Parameters: /// diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index bd5e8e7fa0..c436f81953 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4546,6 +4546,39 @@ } }, "/people": { + "delete": { + "operationId": "deletePeople", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "People" + ] + }, "get": { "operationId": "getAllPeople", "parameters": [ @@ -4711,6 +4744,39 @@ } }, "/people/{id}": { + "delete": { + "operationId": "deletePerson", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "People" + ] + }, "get": { "operationId": "getPerson", "parameters": [ diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index fd866d9122..ff8ea67a18 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2769,6 +2769,15 @@ export function updatePartner({ id, updatePartnerDto }: { body: updatePartnerDto }))); } +export function deletePeople({ bulkIdsDto }: { + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/people", oazapfts.json({ + ...opts, + method: "DELETE", + body: bulkIdsDto + }))); +} export function getAllPeople({ closestAssetId, closestPersonId, page, size, withHidden }: { closestAssetId?: string; closestPersonId?: string; @@ -2813,6 +2822,14 @@ export function updatePeople({ peopleUpdateDto }: { body: peopleUpdateDto }))); } +export function deletePerson({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/people/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} export function getPerson({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { diff --git a/server/src/controllers/person.controller.spec.ts b/server/src/controllers/person.controller.spec.ts index 0366829336..5b63fcc6cd 100644 --- a/server/src/controllers/person.controller.spec.ts +++ b/server/src/controllers/person.controller.spec.ts @@ -60,6 +60,29 @@ describe(PersonController.name, () => { }); }); + describe('DELETE /people', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete('/people'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require uuids in the body', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete('/people') + .send({ ids: ['invalid'] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + + it('should respond with 204', async () => { + const { status } = await request(ctx.getHttpServer()) + .delete(`/people`) + .send({ ids: [factory.uuid()] }); + expect(status).toBe(204); + expect(service.deleteAll).toHaveBeenCalled(); + }); + }); + describe('GET /people/:id', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`); @@ -156,6 +179,25 @@ describe(PersonController.name, () => { }); }); + describe('DELETE /people/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + }); + + it('should respond with 204', async () => { + const { status } = await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`); + expect(status).toBe(204); + expect(service.delete).toHaveBeenCalled(); + }); + }); + describe('POST /people/:id/merge', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`); diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index 3440042eda..a9f6616426 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -1,7 +1,20 @@ -import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Next, + Param, + Post, + Put, + Query, + Res, +} from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceUpdateDto, @@ -49,6 +62,13 @@ export class PersonController { return this.service.updateAll(auth, dto); } + @Delete() + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.PERSON_DELETE }) + deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { + return this.service.deleteAll(auth, dto); + } + @Get(':id') @Authenticated({ permission: Permission.PERSON_READ }) getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { @@ -65,6 +85,13 @@ export class PersonController { return this.service.update(auth, id, dto); } + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.PERSON_DELETE }) + deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); + } + @Get(':id/statistics') @Authenticated({ permission: Permission.PERSON_STATISTICS }) getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 1ac7111d56..cbd952cd04 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -328,3 +328,12 @@ set "deletedAt" = $1 where "asset_faces"."id" = $2 + +-- PersonRepository.getForPeopleDelete +select + "id", + "thumbnailPath" +from + "person" +where + "id" in ($1) diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index a18bf2646a..8fef625a70 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -3,7 +3,7 @@ import { ExpressionBuilder, Insertable, Kysely, Selectable, sql, Updateable } fr import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; import { AssetFaces, DB, FaceSearch, Person } from 'src/db'; -import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; +import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AssetFileType, AssetVisibility, SourceType } from 'src/enum'; import { removeUndefinedKeys } from 'src/utils/database'; import { paginationHelper, PaginationOptions } from 'src/utils/pagination'; @@ -102,6 +102,7 @@ export class PersonRepository { } @GenerateSql({ params: [[DummyValue.UUID]] }) + @Chunked() async delete(ids: string[]): Promise { if (ids.length === 0) { return; @@ -517,4 +518,13 @@ export class PersonRepository { await sql`REINDEX TABLE face_search`.execute(this.db); } } + + @GenerateSql({ params: [[DummyValue.UUID]] }) + @Chunked() + getForPeopleDelete(ids: string[]) { + if (ids.length === 0) { + return Promise.resolve([]); + } + return this.db.selectFrom('person').select(['id', 'thumbnailPath']).where('id', 'in', ids).execute(); + } } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index cd484c230b..61978ba8d5 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -4,7 +4,7 @@ import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { Person } from 'src/database'; import { AssetFaces, FaceSearch } from 'src/db'; import { Chunked, OnJob } from 'src/decorators'; -import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceCreateDto, @@ -216,6 +216,10 @@ export class PersonService extends BaseService { return mapPerson(person); } + delete(auth: AuthDto, id: string): Promise { + return this.deleteAll(auth, { ids: [id] }); + } + async updateAll(auth: AuthDto, dto: PeopleUpdateDto): Promise { const results: BulkIdResponseDto[] = []; for (const person of dto.people) { @@ -236,8 +240,14 @@ export class PersonService extends BaseService { return results; } + async deleteAll(auth: AuthDto, { ids }: BulkIdsDto): Promise { + await this.requireAccess({ auth, permission: Permission.PERSON_DELETE, ids }); + const people = await this.personRepository.getForPeopleDelete(ids); + await this.removeAllPeople(people); + } + @Chunked() - private async delete(people: { id: string; thumbnailPath: string }[]) { + private async removeAllPeople(people: { id: string; thumbnailPath: string }[]) { await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath))); await this.personRepository.delete(people.map((person) => person.id)); this.logger.debug(`Deleted ${people.length} people`); @@ -246,7 +256,7 @@ export class PersonService extends BaseService { @OnJob({ name: JobName.PERSON_CLEANUP, queue: QueueName.BACKGROUND_TASK }) async handlePersonCleanup(): Promise { const people = await this.personRepository.getAllWithoutFaces(); - await this.delete(people); + await this.removeAllPeople(people); return JobStatus.SUCCESS; } @@ -589,7 +599,7 @@ export class PersonService extends BaseService { this.logger.log(`Merging ${mergeName} into ${primaryName}`); await this.personRepository.reassignFaces(mergeData); - await this.delete([mergePerson]); + await this.removeAllPeople([mergePerson]); this.logger.log(`Merged ${mergeName} into ${primaryName}`); results.push({ id: mergeId, success: true }); diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index c1b162927d..b639643b6f 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -256,22 +256,17 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe return access.memory.checkOwnerAccess(auth.user.id, ids); } - case Permission.PERSON_READ: { - return await access.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_UPDATE: { - return await access.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_MERGE: { - return await access.person.checkOwnerAccess(auth.user.id, ids); - } - case Permission.PERSON_CREATE: { return access.person.checkFaceOwnerAccess(auth.user.id, ids); } + case Permission.PERSON_READ: + case Permission.PERSON_UPDATE: + case Permission.PERSON_DELETE: + case Permission.PERSON_MERGE: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + case Permission.PERSON_REASSIGN: { return access.person.checkFaceOwnerAccess(auth.user.id, ids); } diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 988f1b406f..cbecff267d 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -7,6 +7,7 @@ import { AssetFace } from 'src/database'; import { Albums, AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum'; +import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; @@ -24,6 +25,7 @@ import { PartnerRepository } from 'src/repositories/partner.repository'; import { PersonRepository } from 'src/repositories/person.repository'; import { SearchRepository } from 'src/repositories/search.repository'; import { SessionRepository } from 'src/repositories/session.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; import { SyncRepository } from 'src/repositories/sync.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { UserRepository } from 'src/repositories/user.repository'; @@ -40,6 +42,7 @@ const sha256 = (value: string) => createHash('sha256').update(value).digest('bas // type Repositories = Omit; type RepositoriesTypes = { + access: AccessRepository; activity: ActivityRepository; album: AlbumRepository; albumUser: AlbumUserRepository; @@ -58,6 +61,7 @@ type RepositoriesTypes = { person: PersonRepository; search: SearchRepository; session: SessionRepository; + storage: StorageRepository; sync: SyncRepository; systemMetadata: SystemMetadataRepository; versionHistory: VersionHistoryRepository; @@ -180,6 +184,10 @@ export const newMediumService = (key: K, db: Kysely) => { switch (key) { + case 'access': { + return new AccessRepository(db); + } + case 'activity': { return new ActivityRepository(db); } @@ -352,6 +360,10 @@ const getRepositoryMock = (key: K) => { return automock(SessionRepository); } + case 'storage': { + return automock(StorageRepository, { args: [{ setContext: () => {} }] }); + } + case 'sync': { return automock(SyncRepository); } @@ -411,7 +423,7 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.session || getRepositoryMock('session'), repositories.sharedLink, repositories.stack, - repositories.storage, + repositories.storage || getRepositoryMock('storage'), repositories.sync || getRepositoryMock('sync'), repositories.systemMetadata || getRepositoryMock('systemMetadata'), repositories.tag, diff --git a/server/test/medium/specs/services/person.service.spec.ts b/server/test/medium/specs/services/person.service.spec.ts new file mode 100644 index 0000000000..bd5eb5d543 --- /dev/null +++ b/server/test/medium/specs/services/person.service.spec.ts @@ -0,0 +1,90 @@ +import { Kysely } from 'kysely'; +import { DB } from 'src/db'; +import { PersonService } from 'src/services/person.service'; +import { mediumFactory, newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +describe.concurrent(PersonService.name, () => { + let defaultDatabase: Kysely; + + const createSut = (db?: Kysely) => { + return newMediumService(PersonService, { + database: db || defaultDatabase, + repos: { + access: 'real', + database: 'real', + person: 'real', + storage: 'mock', + }, + }); + }; + + beforeEach(async () => { + defaultDatabase = await getKyselyDB(); + }); + + describe('delete', () => { + it('should throw an error when there is no access', async () => { + const { sut } = createSut(); + 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, getRepository, mocks } = createSut(); + + const user = mediumFactory.userInsert(); + const auth = factory.auth({ user }); + const person = mediumFactory.personInsert({ ownerId: auth.user.id }); + mocks.storage.unlink.mockResolvedValue(); + + const userRepo = getRepository('user'); + await userRepo.create(user); + + const personRepo = getRepository('person'); + await personRepo.create(person); + + await expect(personRepo.getById(person.id)).resolves.toEqual(expect.objectContaining({ id: person.id })); + await expect(sut.delete(auth, person.id)).resolves.toBeUndefined(); + await expect(personRepo.getById(person.id)).resolves.toBeUndefined(); + + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); + }); + }); + + describe('deleteAll', () => { + it('should throw an error when there is no access', async () => { + const { sut } = createSut(); + 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, getRepository, mocks } = createSut(); + + const user = mediumFactory.userInsert(); + const auth = factory.auth({ user }); + const person1 = mediumFactory.personInsert({ ownerId: auth.user.id }); + const person2 = mediumFactory.personInsert({ ownerId: auth.user.id }); + mocks.storage.unlink.mockResolvedValue(); + + const userRepo = getRepository('user'); + await userRepo.create(user); + + const personRepo = getRepository('person'); + await personRepo.create(person1); + await personRepo.create(person2); + + await expect(sut.deleteAll(auth, { ids: [person1.id, person2.id] })).resolves.toBeUndefined(); + await expect(personRepo.getById(person1.id)).resolves.toBeUndefined(); + await expect(personRepo.getById(person2.id)).resolves.toBeUndefined(); + + expect(mocks.storage.unlink).toHaveBeenCalledTimes(2); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person1.thumbnailPath); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person2.thumbnailPath); + }); + }); +}); diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts deleted file mode 100644 index 2875c9ada5..0000000000 --- a/server/test/repositories/person.repository.mock.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PersonRepository } from 'src/repositories/person.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newPersonRepositoryMock = (): Mocked> => { - return { - reassignFaces: vitest.fn(), - unassignFaces: vitest.fn(), - delete: vitest.fn(), - deleteFaces: vitest.fn(), - getAllFaces: vitest.fn(), - getAll: vitest.fn(), - getAllForUser: vitest.fn(), - getAllWithoutFaces: vitest.fn(), - getFaces: vitest.fn(), - getFaceById: vitest.fn(), - getFaceForFacialRecognitionJob: vitest.fn(), - getDataForThumbnailGenerationJob: vitest.fn(), - reassignFace: vitest.fn(), - getById: vitest.fn(), - getByName: vitest.fn(), - getDistinctNames: vitest.fn(), - getStatistics: vitest.fn(), - getNumberOfPeople: vitest.fn(), - create: vitest.fn(), - createAll: vitest.fn(), - refreshFaces: vitest.fn(), - update: vitest.fn(), - updateAll: vitest.fn(), - getFacesByIds: vitest.fn(), - getRandomFace: vitest.fn(), - getLatestFaceDate: vitest.fn(), - createAssetFace: vitest.fn(), - deleteAssetFace: vitest.fn(), - softDeleteAssetFaces: vitest.fn(), - vacuum: vitest.fn(), - }; -}; diff --git a/server/test/utils.ts b/server/test/utils.ts index 9f3d3f120d..0e07e603b5 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -67,7 +67,6 @@ import { newDatabaseRepositoryMock } from 'test/repositories/database.repository import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; @@ -278,7 +277,7 @@ export const newTestService = ( notification: automock(NotificationRepository), oauth: automock(OAuthRepository, { args: [loggerMock] }), partner: automock(PartnerRepository, { strict: false }), - person: newPersonRepositoryMock(), + person: automock(PersonRepository, { strict: false }), process: automock(ProcessRepository), search: automock(SearchRepository, { strict: false }), // eslint-disable-next-line no-sparse-arrays