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, }; 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(logger); 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, ); } }