diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 475d82f6028..c86ae8f60e9 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -6,7 +6,6 @@ import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetEntity, searchAssetBuilder } from 'src/entities/asset.entity'; import { AssetStatus, AssetType } from 'src/enum'; -import { LoggingRepository } from 'src/repositories/logging.repository'; import { anyUuid, asUuid } from 'src/utils/database'; import { Paginated } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; @@ -203,12 +202,7 @@ export interface GetCameraMakesOptions { @Injectable() export class SearchRepository { - constructor( - private logger: LoggingRepository, - @InjectKysely() private db: Kysely, - ) { - this.logger.setContext(SearchRepository.name); - } + constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [ diff --git a/server/test/factory.ts b/server/test/factory.ts deleted file mode 100644 index 0e4d272b704..00000000000 --- a/server/test/factory.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { Insertable, Kysely } from 'kysely'; -import { randomBytes } from 'node:crypto'; -import { Writable } from 'node:stream'; -import { AssetFaces, Assets, DB, Person as DbPerson, FaceSearch, Partners, Sessions } from 'src/db'; -import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetType, SourceType } from 'src/enum'; -import { AccessRepository } from 'src/repositories/access.repository'; -import { ActivityRepository } from 'src/repositories/activity.repository'; -import { AlbumRepository } from 'src/repositories/album.repository'; -import { ApiKeyRepository } from 'src/repositories/api-key.repository'; -import { AssetRepository } from 'src/repositories/asset.repository'; -import { AuditRepository } from 'src/repositories/audit.repository'; -import { ConfigRepository } from 'src/repositories/config.repository'; -import { LibraryRepository } from 'src/repositories/library.repository'; -import { LoggingRepository } from 'src/repositories/logging.repository'; -import { MachineLearningRepository } from 'src/repositories/machine-learning.repository'; -import { MediaRepository } from 'src/repositories/media.repository'; -import { MetadataRepository } from 'src/repositories/metadata.repository'; -import { MoveRepository } from 'src/repositories/move.repository'; -import { NotificationRepository } from 'src/repositories/notification.repository'; -import { OAuthRepository } from 'src/repositories/oauth.repository'; -import { PartnerRepository } from 'src/repositories/partner.repository'; -import { PersonRepository } from 'src/repositories/person.repository'; -import { ProcessRepository } from 'src/repositories/process.repository'; -import { SearchRepository } from 'src/repositories/search.repository'; -import { ServerInfoRepository } from 'src/repositories/server-info.repository'; -import { SessionRepository } from 'src/repositories/session.repository'; -import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; -import { StackRepository } from 'src/repositories/stack.repository'; -import { StorageRepository } from 'src/repositories/storage.repository'; -import { SyncRepository } from 'src/repositories/sync.repository'; -import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; -import { TelemetryRepository } from 'src/repositories/telemetry.repository'; -import { TrashRepository } from 'src/repositories/trash.repository'; -import { UserRepository } from 'src/repositories/user.repository'; -import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; -import { ViewRepository } from 'src/repositories/view-repository'; -import { UserTable } from 'src/schema/tables/user.table'; -import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; -import { newDate, newEmbedding, newUuid } from 'test/small.factory'; -import { automock } from 'test/utils'; - -class CustomWritable extends Writable { - private data = ''; - - _write(chunk: any, encoding: string, callback: () => void) { - this.data += chunk.toString(); - callback(); - } - - getResponse() { - const result = this.data; - return result - .split('\n') - .filter((x) => x.length > 0) - .map((x) => JSON.parse(x)); - } -} - -type Asset = Partial>; -type User = Partial>; -type Session = Omit, 'token'> & { token?: string }; -type Partner = Insertable; -type AssetFace = Partial>; -type Person = Partial>; -type Face = Partial>; - -export class TestFactory { - private assets: Asset[] = []; - private sessions: Session[] = []; - private users: User[] = []; - private partners: Partner[] = []; - private assetFaces: AssetFace[] = []; - private persons: Person[] = []; - private faces: Face[] = []; - - private constructor(private context: TestContext) {} - - static create(context: TestContext) { - return new TestFactory(context); - } - - static stream() { - return new CustomWritable(); - } - - static asset(asset: Asset) { - const assetId = asset.id || newUuid(); - const defaults: Insertable = { - deviceAssetId: '', - deviceId: '', - originalFileName: '', - checksum: randomBytes(32), - type: AssetType.IMAGE, - originalPath: '/path/to/something.jpg', - ownerId: '@immich.cloud', - isVisible: true, - fileCreatedAt: new Date('2000-01-01T00:00:00Z'), - fileModifiedAt: new Date('2000-01-01T00:00:00Z'), - localDateTime: new Date('2000-01-01T00:00:00Z'), - }; - - return { - ...defaults, - ...asset, - id: assetId, - }; - } - - static auth(auth: { user: User; session?: Session }) { - return auth as AuthDto; - } - - static user(user: User = {}) { - const userId = user.id || newUuid(); - const defaults: Insertable = { - email: `${userId}@immich.cloud`, - name: `User ${userId}`, - deletedAt: null, - }; - - return { - ...defaults, - ...user, - id: userId, - }; - } - - static session(session: Session) { - const id = session.id || newUuid(); - const defaults = { - token: randomBytes(36).toString('base64url'), - }; - - return { - ...defaults, - ...session, - id, - }; - } - - static partner(partner: Partner) { - const defaults = { - inTimeline: true, - }; - - return { - ...defaults, - ...partner, - }; - } - - static assetFace(assetFace: AssetFace) { - const defaults = { - assetId: assetFace.assetId || newUuid(), - boundingBoxX1: assetFace.boundingBoxX1 || 0, - boundingBoxX2: assetFace.boundingBoxX2 || 1, - boundingBoxY1: assetFace.boundingBoxY1 || 0, - boundingBoxY2: assetFace.boundingBoxY2 || 1, - deletedAt: assetFace.deletedAt || null, - id: assetFace.id || newUuid(), - imageHeight: assetFace.imageHeight || 10, - imageWidth: assetFace.imageWidth || 10, - personId: assetFace.personId || null, - sourceType: assetFace.sourceType || SourceType.MACHINE_LEARNING, - }; - - return { ...defaults, ...assetFace }; - } - - static person(person: Person) { - const defaults = { - birthDate: person.birthDate || null, - color: person.color || null, - createdAt: person.createdAt || newDate(), - faceAssetId: person.faceAssetId || null, - id: person.id || newUuid(), - isFavorite: person.isFavorite || false, - isHidden: person.isHidden || false, - name: person.name || 'Test Name', - ownerId: person.ownerId || newUuid(), - thumbnailPath: person.thumbnailPath || '/path/to/thumbnail.jpg', - updatedAt: person.updatedAt || newDate(), - updateId: person.updateId || newUuid(), - }; - return { ...defaults, ...person }; - } - - static face(face: Face) { - const defaults = { - faceId: face.faceId || newUuid(), - embedding: face.embedding || newEmbedding(), - }; - return { - ...defaults, - ...face, - }; - } - - withAsset(asset: Asset) { - this.assets.push(asset); - return this; - } - - withSession(session: Session) { - this.sessions.push(session); - return this; - } - - withUser(user: User = {}) { - this.users.push(user); - return this; - } - - withPartner(partner: Partner) { - this.partners.push(partner); - return this; - } - - withAssetFace(assetFace: AssetFace) { - this.assetFaces.push(assetFace); - return this; - } - - withPerson(person: Person) { - this.persons.push(person); - return this; - } - - withFaces(face: Face) { - this.faces.push(face); - return this; - } - - async create() { - for (const user of this.users) { - await this.context.createUser(user); - } - - for (const partner of this.partners) { - await this.context.createPartner(partner); - } - - for (const session of this.sessions) { - await this.context.createSession(session); - } - - for (const asset of this.assets) { - await this.context.createAsset(asset); - } - - for (const person of this.persons) { - await this.context.createPerson(person); - } - - await this.context.refreshFaces( - this.assetFaces, - [], - this.faces.map((f) => TestFactory.face(f)), - ); - - return this.context; - } -} - -export class TestContext { - access: AccessRepository; - logger: LoggingRepository; - activity: ActivityRepository; - album: AlbumRepository; - apiKey: ApiKeyRepository; - asset: AssetRepository; - audit: AuditRepository; - config: ConfigRepository; - library: LibraryRepository; - machineLearning: MachineLearningRepository; - media: MediaRepository; - metadata: MetadataRepository; - move: MoveRepository; - notification: NotificationRepository; - oauth: OAuthRepository; - partner: PartnerRepository; - person: PersonRepository; - process: ProcessRepository; - search: SearchRepository; - serverInfo: ServerInfoRepository; - session: SessionRepository; - sharedLink: SharedLinkRepository; - stack: StackRepository; - storage: StorageRepository; - systemMetadata: SystemMetadataRepository; - sync: SyncRepository; - telemetry: TelemetryRepository; - trash: TrashRepository; - user: UserRepository; - versionHistory: VersionHistoryRepository; - view: ViewRepository; - - private constructor(public db: Kysely) { - // eslint-disable-next-line no-sparse-arrays - const logger = automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }); - const config = new ConfigRepository(); - - this.access = new AccessRepository(this.db); - this.logger = logger; - this.activity = new ActivityRepository(this.db); - this.album = new AlbumRepository(this.db); - this.apiKey = new ApiKeyRepository(this.db); - this.asset = new AssetRepository(this.db); - this.audit = new AuditRepository(this.db); - this.config = config; - this.library = new LibraryRepository(this.db); - this.machineLearning = new MachineLearningRepository(logger); - this.media = new MediaRepository(logger); - this.metadata = new MetadataRepository(logger); - this.move = new MoveRepository(this.db); - this.notification = new NotificationRepository(logger); - this.oauth = new OAuthRepository(logger); - this.partner = new PartnerRepository(this.db); - this.person = new PersonRepository(this.db); - this.process = new ProcessRepository(); - this.search = new SearchRepository(logger, this.db); - this.serverInfo = new ServerInfoRepository(config, logger); - this.session = new SessionRepository(this.db); - this.sharedLink = new SharedLinkRepository(this.db); - this.stack = new StackRepository(this.db); - this.storage = new StorageRepository(logger); - this.sync = new SyncRepository(this.db); - this.systemMetadata = new SystemMetadataRepository(this.db); - this.telemetry = newTelemetryRepositoryMock() as unknown as TelemetryRepository; - this.trash = new TrashRepository(this.db); - this.user = new UserRepository(this.db); - this.versionHistory = new VersionHistoryRepository(this.db); - this.view = new ViewRepository(this.db); - } - - static from(db: Kysely) { - return new TestContext(db).getFactory(); - } - - getFactory() { - return TestFactory.create(this); - } - - createUser(user: User = {}) { - return this.user.create(TestFactory.user(user)); - } - - createPartner(partner: Partner) { - return this.partner.create(TestFactory.partner(partner)); - } - - createAsset(asset: Asset) { - return this.asset.create(TestFactory.asset(asset)); - } - - createSession(session: Session) { - return this.session.create(TestFactory.session(session)); - } - - createPerson(person: Person) { - return this.person.create(TestFactory.person(person)); - } - - refreshFaces(facesToAdd: AssetFace[], faceIdsToRemove: string[], embeddingsToAdd?: Insertable[]) { - return this.person.refreshFaces( - facesToAdd.map((f) => TestFactory.assetFace(f)), - faceIdsToRemove, - embeddingsToAdd, - ); - } -} diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index a1d05ccd1c8..ce356b898b7 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -1,9 +1,11 @@ import { ClassConstructor } from 'class-transformer'; import { Insertable, Kysely } from 'kysely'; import { DateTime } from 'luxon'; -import { randomBytes } from 'node:crypto'; -import { AssetJobStatus, Assets, DB } from 'src/db'; -import { AssetType } from 'src/enum'; +import { createHash, randomBytes } from 'node:crypto'; +import { Writable } from 'node:stream'; +import { AssetFace } from 'src/database'; +import { AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db'; +import { AssetType, SourceType } from 'src/enum'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; @@ -15,17 +17,22 @@ import { JobRepository } from 'src/repositories/job.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; 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 { SyncRepository } from 'src/repositories/sync.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { UserRepository } from 'src/repositories/user.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { UserTable } from 'src/schema/tables/user.table'; import { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; -import { newDate, newUuid } from 'test/small.factory'; +import { newDate, newEmbedding, newUuid } from 'test/small.factory'; import { automock, ServiceOverrides } from 'test/utils'; import { Mocked } from 'vitest'; +const sha256 = (value: string) => createHash('sha256').update(value).digest('base64'); + // type Repositories = Omit; type Repositories = { activity: ActivityRepository; @@ -40,7 +47,10 @@ type Repositories = { logger: LoggingRepository; memory: MemoryRepository; partner: PartnerRepository; + person: PersonRepository; + search: SearchRepository; session: SessionRepository; + sync: SyncRepository; systemMetadata: SystemMetadataRepository; versionHistory: VersionHistoryRepository; }; @@ -145,10 +155,22 @@ export const getRepository = (key: K, db: Kysely(key: K) => { return automock(PartnerRepository); } + case 'person': { + return automock(PersonRepository); + } + case 'session': { return automock(SessionRepository); } + case 'sync': { + return automock(SyncRepository); + } + case 'systemMetadata': { return automock(SystemMetadataRepository); } @@ -266,7 +296,7 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.notification, repositories.oauth, repositories.partner || getRepositoryMock('partner'), - repositories.person, + repositories.person || getRepositoryMock('person'), repositories.process, repositories.search, repositories.serverInfo, @@ -274,7 +304,7 @@ export const asDeps = (repositories: ServiceOverrides) => { repositories.sharedLink, repositories.stack, repositories.storage, - repositories.sync, + repositories.sync || getRepositoryMock('sync'), repositories.systemMetadata || getRepositoryMock('systemMetadata'), repositories.tag, repositories.telemetry, @@ -297,6 +327,7 @@ const assetInsert = (asset: Partial> = {}) => { originalPath: '/path/to/something.jpg', ownerId: '@immich.cloud', isVisible: true, + isFavorite: false, fileCreatedAt: now, fileModifiedAt: now, localDateTime: now, @@ -309,6 +340,38 @@ const assetInsert = (asset: Partial> = {}) => { }; }; +const faceInsert = (face: Partial> & { faceId: string }) => { + const defaults = { + faceId: face.faceId, + embedding: face.embedding || newEmbedding(), + }; + return { + ...defaults, + ...face, + }; +}; + +const assetFaceInsert = (assetFace: Partial & { assetId: string }) => { + const defaults = { + assetId: assetFace.assetId ?? newUuid(), + boundingBoxX1: assetFace.boundingBoxX1 ?? 0, + boundingBoxX2: assetFace.boundingBoxX2 ?? 1, + boundingBoxY1: assetFace.boundingBoxY1 ?? 0, + boundingBoxY2: assetFace.boundingBoxY2 ?? 1, + deletedAt: assetFace.deletedAt ?? null, + id: assetFace.id ?? newUuid(), + imageHeight: assetFace.imageHeight ?? 10, + imageWidth: assetFace.imageWidth ?? 10, + personId: assetFace.personId ?? null, + sourceType: assetFace.sourceType ?? SourceType.MACHINE_LEARNING, + }; + + return { + ...defaults, + ...assetFace, + }; +}; + const assetJobStatusInsert = ( job: Partial> & { assetId: string }, ): Insertable => { @@ -327,6 +390,41 @@ const assetJobStatusInsert = ( }; }; +const personInsert = (person: Partial> & { ownerId: string }) => { + const defaults = { + birthDate: person.birthDate || null, + color: person.color || null, + createdAt: person.createdAt || newDate(), + faceAssetId: person.faceAssetId || null, + id: person.id || newUuid(), + isFavorite: person.isFavorite || false, + isHidden: person.isHidden || false, + name: person.name || 'Test Name', + ownerId: person.ownerId || newUuid(), + thumbnailPath: person.thumbnailPath || '/path/to/thumbnail.jpg', + updatedAt: person.updatedAt || newDate(), + updateId: person.updateId || newUuid(), + }; + return { + ...defaults, + ...person, + }; +}; + +const sessionInsert = ({ id = newUuid(), userId, ...session }: Partial> & { userId: string }) => { + const defaults: Insertable = { + id, + userId, + token: sha256(id), + }; + + return { + ...defaults, + ...session, + id, + }; +}; + const userInsert = (user: Partial> = {}) => { const id = user.id || newUuid(); @@ -339,8 +437,34 @@ const userInsert = (user: Partial> = {}) => { return { ...defaults, ...user, id }; }; +class CustomWritable extends Writable { + private data = ''; + + _write(chunk: any, encoding: string, callback: () => void) { + this.data += chunk.toString(); + callback(); + } + + getResponse() { + const result = this.data; + return result + .split('\n') + .filter((x) => x.length > 0) + .map((x) => JSON.parse(x)); + } +} + +const syncStream = () => { + return new CustomWritable(); +}; + export const mediumFactory = { assetInsert, + assetFaceInsert, assetJobStatusInsert, + faceInsert, + personInsert, + sessionInsert, + syncStream, userInsert, }; diff --git a/server/test/medium/specs/person.service.spec.ts b/server/test/medium/specs/person.service.spec.ts deleted file mode 100644 index e79564b4371..00000000000 --- a/server/test/medium/specs/person.service.spec.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Kysely } from 'kysely'; -import { JobStatus, SourceType } from 'src/enum'; -import { PersonService } from 'src/services/person.service'; -import { TestContext, TestFactory } from 'test/factory'; -import { newEmbedding } from 'test/small.factory'; -import { getKyselyDB, newTestService } from 'test/utils'; - -const setup = async (db: Kysely) => { - const context = await TestContext.from(db).create(); - const { sut, mocks } = newTestService(PersonService, context); - - return { sut, mocks, context }; -}; - -describe.concurrent(PersonService.name, () => { - let sut: PersonService; - let context: TestContext; - - beforeAll(async () => { - ({ sut, context } = await setup(await getKyselyDB())); - }); - - describe('handleRecognizeFaces', () => { - it('should skip if face source type is not MACHINE_LEARNING', async () => { - const user = TestFactory.user(); - const asset = TestFactory.asset({ ownerId: user.id }); - const assetFace = TestFactory.assetFace({ assetId: asset.id, sourceType: SourceType.MANUAL }); - const face = TestFactory.face({ faceId: assetFace.id }); - await context.getFactory().withUser(user).withAsset(asset).withAssetFace(assetFace).withFaces(face).create(); - - const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false }); - - expect(result).toBe(JobStatus.SKIPPED); - const newPersonId = await context.db - .selectFrom('asset_faces') - .select('asset_faces.personId') - .where('asset_faces.id', '=', assetFace.id) - .executeTakeFirst(); - expect(newPersonId?.personId).toBeNull(); - }); - - it('should fail if face does not have an embedding', async () => { - const user = TestFactory.user(); - const asset = TestFactory.asset({ ownerId: user.id }); - const assetFace = TestFactory.assetFace({ assetId: asset.id, sourceType: SourceType.MACHINE_LEARNING }); - await context.getFactory().withUser(user).withAsset(asset).withAssetFace(assetFace).create(); - - const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false }); - - expect(result).toBe(JobStatus.FAILED); - const newPersonId = await context.db - .selectFrom('asset_faces') - .select('asset_faces.personId') - .where('asset_faces.id', '=', assetFace.id) - .executeTakeFirst(); - expect(newPersonId?.personId).toBeNull(); - }); - - it('should skip if face already has a person assigned', async () => { - const user = TestFactory.user(); - const asset = TestFactory.asset({ ownerId: user.id }); - const person = TestFactory.person({ ownerId: user.id }); - const assetFace = TestFactory.assetFace({ - assetId: asset.id, - sourceType: SourceType.MACHINE_LEARNING, - personId: person.id, - }); - const face = TestFactory.face({ faceId: assetFace.id }); - await context - .getFactory() - .withUser(user) - .withAsset(asset) - .withPerson(person) - .withAssetFace(assetFace) - .withFaces(face) - .create(); - - const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false }); - - expect(result).toBe(JobStatus.SKIPPED); - const newPersonId = await context.db - .selectFrom('asset_faces') - .select('asset_faces.personId') - .where('asset_faces.id', '=', assetFace.id) - .executeTakeFirst(); - expect(newPersonId?.personId).toEqual(person.id); - }); - - it('should create a new person if no matches are found', async () => { - const user = TestFactory.user(); - const embedding = newEmbedding(); - - let factory = context.getFactory().withUser(user); - - for (let i = 0; i < 3; i++) { - const existingAsset = TestFactory.asset({ ownerId: user.id }); - const existingAssetFace = TestFactory.assetFace({ - assetId: existingAsset.id, - sourceType: SourceType.MACHINE_LEARNING, - }); - const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding }); - factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace); - } - - const newAsset = TestFactory.asset({ ownerId: user.id }); - const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING }); - const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding }); - - await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create(); - - const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false }); - - expect(result).toBe(JobStatus.SUCCESS); - - const newPersonId = await context.db - .selectFrom('asset_faces') - .select('asset_faces.personId') - .where('asset_faces.id', '=', newAssetFace.id) - .executeTakeFirstOrThrow(); - expect(newPersonId.personId).toBeDefined(); - }); - - it('should assign face to an existing person if matches are found', async () => { - const user = TestFactory.user(); - const existingPerson = TestFactory.person({ ownerId: user.id }); - const embedding = newEmbedding(); - - let factory = context.getFactory().withUser(user).withPerson(existingPerson); - - const assetFaces: string[] = []; - - for (let i = 0; i < 3; i++) { - const existingAsset = TestFactory.asset({ ownerId: user.id }); - const existingAssetFace = TestFactory.assetFace({ - assetId: existingAsset.id, - sourceType: SourceType.MACHINE_LEARNING, - }); - assetFaces.push(existingAssetFace.id); - const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding }); - factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace); - } - - const newAsset = TestFactory.asset({ ownerId: user.id }); - const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING }); - const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding }); - await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create(); - await context.person.reassignFaces({ newPersonId: existingPerson.id, faceIds: assetFaces }); - - const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false }); - - expect(result).toBe(JobStatus.SUCCESS); - - const after = await context.db - .selectFrom('asset_faces') - .select('asset_faces.personId') - .where('asset_faces.id', '=', newAssetFace.id) - .executeTakeFirstOrThrow(); - expect(after.personId).toEqual(existingPerson.id); - }); - - it('should not assign face to an existing person if asset is older than person', async () => { - const user = TestFactory.user(); - const assetCreatedAt = new Date('2020-02-23T05:06:29.716Z'); - const birthDate = new Date(assetCreatedAt.getTime() + 3600 * 1000 * 365); - const existingPerson = TestFactory.person({ ownerId: user.id, birthDate }); - const embedding = newEmbedding(); - - let factory = context.getFactory().withUser(user).withPerson(existingPerson); - - const assetFaces: string[] = []; - - for (let i = 0; i < 3; i++) { - const existingAsset = TestFactory.asset({ ownerId: user.id }); - const existingAssetFace = TestFactory.assetFace({ - assetId: existingAsset.id, - sourceType: SourceType.MACHINE_LEARNING, - }); - assetFaces.push(existingAssetFace.id); - const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding }); - factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace); - } - - const newAsset = TestFactory.asset({ ownerId: user.id, fileCreatedAt: assetCreatedAt }); - const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING }); - const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding }); - await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create(); - await context.person.reassignFaces({ newPersonId: existingPerson.id, faceIds: assetFaces }); - - const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false }); - - expect(result).toBe(JobStatus.SKIPPED); - - const after = await context.db - .selectFrom('asset_faces') - .select('asset_faces.personId') - .where('asset_faces.id', '=', newAssetFace.id) - .executeTakeFirstOrThrow(); - expect(after.personId).toBeNull(); - }); - }); -}); diff --git a/server/test/medium/specs/sync.service.spec.ts b/server/test/medium/specs/sync.service.spec.ts index 12ec4079fe5..98df296cbfa 100644 --- a/server/test/medium/specs/sync.service.spec.ts +++ b/server/test/medium/specs/sync.service.spec.ts @@ -1,22 +1,37 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { SyncEntityType, SyncRequestType } from 'src/enum'; import { SYNC_TYPES_ORDER, SyncService } from 'src/services/sync.service'; -import { TestContext, TestFactory } from 'test/factory'; -import { getKyselyDB, newTestService } from 'test/utils'; +import { mediumFactory, newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; const setup = async () => { - const user = TestFactory.user(); - const session = TestFactory.session({ userId: user.id }); - const auth = TestFactory.auth({ session, user }); - const db = await getKyselyDB(); - const context = await TestContext.from(db).withUser(user).withSession(session).create(); + const { sut, mocks, repos, getRepository } = newMediumService(SyncService, { + database: db, + repos: { + sync: 'real', + session: 'real', + }, + }); - const { sut } = newTestService(SyncService, context); + const user = mediumFactory.userInsert(); + const session = mediumFactory.sessionInsert({ userId: user.id }); + const auth = factory.auth({ + session, + user: { + id: user.id, + name: user.name, + email: user.email, + }, + }); + + await getRepository('user').create(user); + await getRepository('session').create(session); const testSync = async (auth: AuthDto, types: SyncRequestType[]) => { - const stream = TestFactory.stream(); + const stream = mediumFactory.syncStream(); // Wait for 1ms to ensure all updates are available await new Promise((resolve) => setTimeout(resolve, 1)); await sut.stream(auth, stream, { types }); @@ -25,9 +40,11 @@ const setup = async () => { }; return { - auth, - context, sut, + auth, + mocks, + repos, + getRepository, testSync, }; }; @@ -43,9 +60,10 @@ describe(SyncService.name, () => { describe.concurrent(SyncEntityType.UserV1, () => { it('should detect and sync the first user', async () => { - const { context, auth, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); - const user = await context.user.get(auth.user.id, { withDeleted: false }); + const userRepo = getRepository('user'); + const user = await userRepo.get(auth.user.id, { withDeleted: false }); if (!user) { expect.fail('First user should exist'); } @@ -73,10 +91,11 @@ describe(SyncService.name, () => { }); it('should detect and sync a soft deleted user', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); const deletedAt = new Date().toISOString(); - const deleted = await context.createUser({ deletedAt }); + const deletedUser = mediumFactory.userInsert({ deletedAt }); + const deleted = await getRepository('user').create(deletedUser); const response = await testSync(auth, [SyncRequestType.UsersV1]); @@ -114,10 +133,12 @@ describe(SyncService.name, () => { }); it('should detect and sync a deleted user', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); - const user = await context.createUser(); - await context.user.delete({ id: user.id }, true); + const userRepo = getRepository('user'); + const user = mediumFactory.userInsert(); + await userRepo.create(user); + await userRepo.delete({ id: user.id }, true); const response = await testSync(auth, [SyncRequestType.UsersV1]); @@ -152,7 +173,7 @@ describe(SyncService.name, () => { }); it('should sync a user and then an update to that same user', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); @@ -175,8 +196,8 @@ describe(SyncService.name, () => { const acks = [initialSyncResponse[0].ack]; await sut.setAcks(auth, { acks }); - const updated = await context.user.update(auth.user.id, { name: 'new name' }); - + const userRepo = getRepository('user'); + const updated = await userRepo.update(auth.user.id, { name: 'new name' }); const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); expect(updatedSyncResponse).toHaveLength(1); @@ -199,12 +220,16 @@ describe(SyncService.name, () => { describe.concurrent(SyncEntityType.PartnerV1, () => { it('should detect and sync the first partner', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); const user1 = auth.user; - const user2 = await context.createUser(); + const userRepo = getRepository('user'); + const partnerRepo = getRepository('partner'); - const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); @@ -232,13 +257,16 @@ describe(SyncService.name, () => { }); it('should detect and sync a deleted partner', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); + const userRepo = getRepository('user'); const user1 = auth.user; - const user2 = await context.createUser(); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); - const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); - await context.partner.remove(partner); + const partnerRepo = getRepository('partner'); + const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); + await partnerRepo.remove(partner); const response = await testSync(auth, [SyncRequestType.PartnersV1]); @@ -265,13 +293,15 @@ describe(SyncService.name, () => { }); it('should detect and sync a partner share both to and from another user', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); + const userRepo = getRepository('user'); const user1 = auth.user; - const user2 = await context.createUser(); + const user2 = await userRepo.create(mediumFactory.userInsert()); - const partner1 = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); - const partner2 = await context.createPartner({ sharedById: user1.id, sharedWithId: user2.id }); + const partnerRepo = getRepository('partner'); + const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); + const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id }); const response = await testSync(auth, [SyncRequestType.PartnersV1]); @@ -307,12 +337,14 @@ describe(SyncService.name, () => { }); it('should sync a partner and then an update to that same partner', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); + const userRepo = getRepository('user'); const user1 = auth.user; - const user2 = await context.createUser(); + const user2 = await userRepo.create(mediumFactory.userInsert()); - const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); + const partnerRepo = getRepository('partner'); + const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); @@ -334,7 +366,7 @@ describe(SyncService.name, () => { const acks = [initialSyncResponse[0].ack]; await sut.setAcks(auth, { acks }); - const updated = await context.partner.update( + const updated = await partnerRepo.update( { sharedById: partner.sharedById, sharedWithId: partner.sharedWithId }, { inTimeline: true }, ); @@ -358,26 +390,31 @@ describe(SyncService.name, () => { }); it('should not sync a partner or partner delete for an unrelated user', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - const user3 = await context.createUser(); + const userRepo = getRepository('user'); + const user2 = await userRepo.create(mediumFactory.userInsert()); + const user3 = await userRepo.create(mediumFactory.userInsert()); - await context.createPartner({ sharedById: user2.id, sharedWithId: user3.id }); + const partnerRepo = getRepository('partner'); + const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id }); expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); - await context.partner.remove({ sharedById: user2.id, sharedWithId: user3.id }); + await partnerRepo.remove(partner); expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); }); it('should not sync a partner delete after a user is deleted', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - await context.createPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); - await context.user.delete({ id: user2.id }, true); + const userRepo = getRepository('user'); + const user2 = await userRepo.create(mediumFactory.userInsert()); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + await userRepo.delete({ id: user2.id }, true); expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); }); @@ -385,21 +422,23 @@ describe(SyncService.name, () => { describe.concurrent(SyncEntityType.AssetV1, () => { it('should detect and sync the first asset', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); - const asset = TestFactory.asset({ + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id, checksum: Buffer.from(checksum, 'base64'), thumbhash: Buffer.from(thumbhash, 'base64'), fileCreatedAt: date, fileModifiedAt: date, + localDateTime: date, deletedAt: null, }); - await context.createAsset(asset); + await assetRepo.create(asset); const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); @@ -413,12 +452,12 @@ describe(SyncService.name, () => { ownerId: asset.ownerId, thumbhash, checksum, - deletedAt: null, - fileCreatedAt: date, - fileModifiedAt: date, - isFavorite: false, - isVisible: true, - localDateTime: '2000-01-01T00:00:00.000Z', + deletedAt: asset.deletedAt, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + isFavorite: asset.isFavorite, + isVisible: asset.isVisible, + localDateTime: asset.localDateTime, type: asset.type, }, type: 'AssetV1', @@ -435,11 +474,12 @@ describe(SyncService.name, () => { }); it('should detect and sync a deleted asset', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); - const asset = TestFactory.asset({ ownerId: auth.user.id }); - await context.createAsset(asset); - await context.asset.remove(asset); + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); + await assetRepo.create(asset); + await assetRepo.remove(asset); const response = await testSync(auth, [SyncRequestType.AssetsV1]); @@ -465,19 +505,26 @@ describe(SyncService.name, () => { }); it('should not sync an asset or asset delete for an unrelated user', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - const session = TestFactory.session({ userId: user2.id }); - const auth2 = TestFactory.auth({ session, user: user2 }); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); - const asset = TestFactory.asset({ ownerId: user2.id }); - await context.createAsset(asset); + const sessionRepo = getRepository('session'); + const session = mediumFactory.sessionInsert({ userId: user2.id }); + await sessionRepo.create(session); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + await assetRepo.create(asset); + + const auth2 = factory.auth({ session, user: user2 }); expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); - await context.asset.remove(asset); + await assetRepo.remove(asset); expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); }); @@ -485,24 +532,30 @@ describe(SyncService.name, () => { describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { it('should detect and sync the first partner asset', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); - const user2 = await context.createUser(); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); - const asset = TestFactory.asset({ + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id, checksum: Buffer.from(checksum, 'base64'), thumbhash: Buffer.from(thumbhash, 'base64'), fileCreatedAt: date, fileModifiedAt: date, + localDateTime: date, deletedAt: null, }); - await context.createAsset(asset); - await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + await assetRepo.create(asset); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); @@ -521,7 +574,7 @@ describe(SyncService.name, () => { fileModifiedAt: date, isFavorite: false, isVisible: true, - localDateTime: '2000-01-01T00:00:00.000Z', + localDateTime: date, type: asset.type, }, type: SyncEntityType.PartnerAssetV1, @@ -538,13 +591,19 @@ describe(SyncService.name, () => { }); it('should detect and sync a deleted partner asset', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - const asset = TestFactory.asset({ ownerId: user2.id }); - await context.createAsset(asset); - await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - await context.asset.remove(asset); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + + const assetRepo = getRepository('asset'); + await assetRepo.create(asset); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + await assetRepo.remove(asset); const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); @@ -570,62 +629,89 @@ describe(SyncService.name, () => { }); it('should not sync a deleted partner asset due to a user delete', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - await context.createAsset({ ownerId: user2.id }); - await context.user.delete({ id: user2.id }, true); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id })); + + await userRepo.delete({ id: user2.id }, true); const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(0); }); it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - await context.createAsset({ ownerId: user2.id }); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const assetRepo = getRepository('asset'); + await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id })); + + const partnerRepo = getRepository('partner'); const partner = { sharedById: user2.id, sharedWithId: auth.user.id }; - await context.partner.create(partner); + await partnerRepo.create(partner); await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1); - await context.partner.remove(partner); + await partnerRepo.remove(partner); await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); }); it('should not sync an asset or asset delete for own user', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - const asset = await context.createAsset({ ownerId: auth.user.id }); - const partner = { sharedById: user2.id, sharedWithId: auth.user.id }; - await context.partner.create(partner); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); + await assetRepo.create(asset); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); - await context.asset.remove(asset); + await assetRepo.remove(asset); await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); }); it('should not sync an asset or asset delete for unrelated user', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - const session = TestFactory.session({ userId: user2.id }); - const auth2 = TestFactory.auth({ session, user: user2 }); - const asset = await context.createAsset({ ownerId: user2.id }); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const sessionRepo = getRepository('session'); + const session = mediumFactory.sessionInsert({ userId: user2.id }); + await sessionRepo.create(session); + + const auth2 = factory.auth({ session, user: user2 }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + await assetRepo.create(asset); await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); - await context.asset.remove(asset); + await assetRepo.remove(asset); await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); @@ -634,13 +720,12 @@ describe(SyncService.name, () => { describe.concurrent(SyncRequestType.AssetExifsV1, () => { it('should detect and sync the first asset exif', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); - const asset = TestFactory.asset({ ownerId: auth.user.id }); - const exif = { assetId: asset.id, make: 'Canon' }; - - await context.createAsset(asset); - await context.asset.upsertExif(exif); + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]); @@ -690,19 +775,25 @@ describe(SyncService.name, () => { }); it('should only sync asset exif for own user', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - const session = TestFactory.session({ userId: user2.id }); - const auth2 = TestFactory.auth({ session, user: user2 }); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); - await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - const asset = TestFactory.asset({ ownerId: user2.id }); - const exif = { assetId: asset.id, make: 'Canon' }; + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - await context.createAsset(asset); - await context.asset.upsertExif(exif); + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); + const sessionRepo = getRepository('session'); + const session = mediumFactory.sessionInsert({ userId: user2.id }); + await sessionRepo.create(session); + + const auth2 = factory.auth({ session, user: user2 }); await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0); }); @@ -710,14 +801,19 @@ describe(SyncService.name, () => { describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => { it('should detect and sync the first partner asset exif', async () => { - const { auth, context, sut, testSync } = await setup(); + const { auth, sut, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - const asset = TestFactory.asset({ ownerId: user2.id }); - await context.createAsset(asset); - const exif = { assetId: asset.id, make: 'Canon' }; - await context.asset.upsertExif(exif); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user2.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); @@ -767,32 +863,46 @@ describe(SyncService.name, () => { }); it('should not sync partner asset exif for own user', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - const asset = TestFactory.asset({ ownerId: auth.user.id }); - const exif = { assetId: asset.id, make: 'Canon' }; - await context.createAsset(asset); - await context.asset.upsertExif(exif); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + await userRepo.create(user2); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); }); it('should not sync partner asset exif for unrelated user', async () => { - const { auth, context, testSync } = await setup(); + const { auth, getRepository, testSync } = await setup(); - const user2 = await context.createUser(); - const user3 = await context.createUser(); - const session = TestFactory.session({ userId: user3.id }); - const authUser3 = TestFactory.auth({ session, user: user3 }); - await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - const asset = TestFactory.asset({ ownerId: user3.id }); - const exif = { assetId: asset.id, make: 'Canon' }; - await context.createAsset(asset); - await context.asset.upsertExif(exif); + const userRepo = getRepository('user'); + const user2 = mediumFactory.userInsert(); + const user3 = mediumFactory.userInsert(); + await Promise.all([userRepo.create(user2), userRepo.create(user3)]); + + const partnerRepo = getRepository('partner'); + await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + + const assetRepo = getRepository('asset'); + const asset = mediumFactory.assetInsert({ ownerId: user3.id }); + await assetRepo.create(asset); + await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); + + const sessionRepo = getRepository('session'); + const session = mediumFactory.sessionInsert({ userId: user3.id }); + await sessionRepo.create(session); + + const authUser3 = factory.auth({ session, user: user3 }); await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); }); diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 313660bff40..3337106b93d 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -41,7 +41,10 @@ const authFactory = ({ }: { apiKey?: Partial; session?: { id: string }; - user?: Partial; + user?: Omit< + Partial, + 'createdAt' | 'updatedAt' | 'deletedAt' | 'fileCreatedAt' | 'fileModifiedAt' | 'localDateTime' | 'profileChangedAt' + >; sharedLink?: Partial; } = {}) => { const auth: AuthDto = { diff --git a/server/test/utils.ts b/server/test/utils.ts index 42bb52f37a0..ff56a12feb3 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -202,7 +202,7 @@ export const newTestService = ( partner: automock(PartnerRepository, { strict: false }), person: newPersonRepositoryMock(), process: automock(ProcessRepository), - search: automock(SearchRepository, { args: [loggerMock], strict: false }), + search: automock(SearchRepository, { strict: false }), // eslint-disable-next-line no-sparse-arrays serverInfo: automock(ServerInfoRepository, { args: [, loggerMock], strict: false }), session: automock(SessionRepository),