import { Kysely, sql } from 'kysely'; import { PostgresJSDialect } from 'kysely-postgres-js'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { Writable } from 'node:stream'; import { parse } from 'pg-connection-string'; import { PNG } from 'pngjs'; import postgres, { Notice } from 'postgres'; import { DB } from 'src/db'; import { ImmichWorker } 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'; 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 { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { MachineLearningRepository } from 'src/repositories/machine-learning.repository'; import { MapRepository } from 'src/repositories/map.repository'; import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.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 { TagRepository } from 'src/repositories/tag.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 { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock'; import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newCronRepositoryMock } from 'test/repositories/cron.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; import { newMapRepositoryMock } from 'test/repositories/map.repository.mock'; import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock'; import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock'; import { newOAuthRepositoryMock } from 'test/repositories/oauth.repository.mock'; import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; import { newProcessRepositoryMock } from 'test/repositories/process.repository.mock'; import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock'; import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { newSyncRepositoryMock } from 'test/repositories/sync.repository.mock'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { newTrashRepositoryMock } from 'test/repositories/trash.repository.mock'; import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock'; import { newViewRepositoryMock } from 'test/repositories/view.repository.mock'; import { Readable } from 'typeorm/platform/PlatformTools'; import { Mocked, vitest } from 'vitest'; type Overrides = { worker?: ImmichWorker; metadataRepository?: MetadataRepository; syncRepository?: SyncRepository; userRepository?: UserRepository; }; type BaseServiceArgs = ConstructorParameters; type Constructor> = { new (...deps: Args): Type; }; type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface }; export type ServiceMocks = { access: IAccessRepositoryMock; activity: Mocked>; album: Mocked>; albumUser: Mocked>; apiKey: Mocked>; audit: Mocked>; asset: Mocked>; config: Mocked>; cron: Mocked>; crypto: Mocked>; database: Mocked>; event: Mocked>; job: Mocked>; library: Mocked>; logger: Mocked; machineLearning: Mocked>; map: Mocked>; media: Mocked>; memory: Mocked>; metadata: Mocked>; move: Mocked>; notification: Mocked>; oauth: Mocked>; partner: Mocked>; person: Mocked>; process: Mocked>; search: Mocked>; serverInfo: Mocked>; session: Mocked>; sharedLink: Mocked>; stack: Mocked>; storage: Mocked>; systemMetadata: Mocked>; tag: Mocked>; telemetry: ITelemetryRepositoryMock; trash: Mocked>; user: Mocked>; versionHistory: Mocked>; view: Mocked>; }; export const newTestService = ( Service: Constructor, overrides?: Overrides, ) => { const { metadataRepository, userRepository, syncRepository } = overrides || {}; const accessMock = newAccessRepositoryMock(); const loggerMock = newLoggingRepositoryMock(); const cronMock = newCronRepositoryMock(); const cryptoMock = newCryptoRepositoryMock(); const activityMock = newActivityRepositoryMock(); const auditMock = newAuditRepositoryMock(); const albumMock = newAlbumRepositoryMock(); const albumUserMock = newAlbumUserRepositoryMock(); const assetMock = newAssetRepositoryMock(); const configMock = newConfigRepositoryMock(); const databaseMock = newDatabaseRepositoryMock(); const eventMock = newEventRepositoryMock(); const jobMock = newJobRepositoryMock(); const apiKeyMock = newKeyRepositoryMock(); const libraryMock = newLibraryRepositoryMock(); const machineLearningMock = newMachineLearningRepositoryMock(); const mapMock = newMapRepositoryMock(); const mediaMock = newMediaRepositoryMock(); const memoryMock = newMemoryRepositoryMock(); const metadataMock = (metadataRepository || newMetadataRepositoryMock()) as Mocked< RepositoryInterface >; const moveMock = newMoveRepositoryMock(); const notificationMock = newNotificationRepositoryMock(); const oauthMock = newOAuthRepositoryMock(); const partnerMock = newPartnerRepositoryMock(); const personMock = newPersonRepositoryMock(); const processMock = newProcessRepositoryMock(); const searchMock = newSearchRepositoryMock(); const serverInfoMock = newServerInfoRepositoryMock(); const sessionMock = newSessionRepositoryMock(); const sharedLinkMock = newSharedLinkRepositoryMock(); const stackMock = newStackRepositoryMock(); const storageMock = newStorageRepositoryMock(); const syncMock = (syncRepository || newSyncRepositoryMock()) as Mocked>; const systemMock = newSystemMetadataRepositoryMock(); const tagMock = newTagRepositoryMock(); const telemetryMock = newTelemetryRepositoryMock(); const trashMock = newTrashRepositoryMock(); const userMock = (userRepository || newUserRepositoryMock()) as Mocked>; const versionHistoryMock = newVersionHistoryRepositoryMock(); const viewMock = newViewRepositoryMock(); const sut = new Service( loggerMock as ILoggingRepository as LoggingRepository, accessMock as IAccessRepository as AccessRepository, activityMock as RepositoryInterface as ActivityRepository, auditMock as RepositoryInterface as AuditRepository, albumMock as RepositoryInterface as AlbumRepository, albumUserMock as RepositoryInterface as AlbumUserRepository, assetMock as RepositoryInterface as AssetRepository, configMock as RepositoryInterface as ConfigRepository, cronMock as RepositoryInterface as CronRepository, cryptoMock as RepositoryInterface as CryptoRepository, databaseMock as RepositoryInterface as DatabaseRepository, eventMock as RepositoryInterface as EventRepository, jobMock as RepositoryInterface as JobRepository, apiKeyMock as RepositoryInterface as ApiKeyRepository, libraryMock as RepositoryInterface as LibraryRepository, machineLearningMock as RepositoryInterface as MachineLearningRepository, mapMock as RepositoryInterface as MapRepository, mediaMock as RepositoryInterface as MediaRepository, memoryMock as RepositoryInterface as MemoryRepository, metadataMock as RepositoryInterface as MetadataRepository, moveMock as RepositoryInterface as MoveRepository, notificationMock as RepositoryInterface as NotificationRepository, oauthMock as RepositoryInterface as OAuthRepository, partnerMock as RepositoryInterface as PartnerRepository, personMock as RepositoryInterface as PersonRepository, processMock as RepositoryInterface as ProcessRepository, searchMock as RepositoryInterface as SearchRepository, serverInfoMock as RepositoryInterface as ServerInfoRepository, sessionMock as RepositoryInterface as SessionRepository, sharedLinkMock as RepositoryInterface as SharedLinkRepository, stackMock as RepositoryInterface as StackRepository, storageMock as RepositoryInterface as StorageRepository, syncMock as RepositoryInterface as SyncRepository, systemMock as RepositoryInterface as SystemMetadataRepository, tagMock as RepositoryInterface as TagRepository, telemetryMock as unknown as TelemetryRepository, trashMock as RepositoryInterface as TrashRepository, userMock as RepositoryInterface as UserRepository, versionHistoryMock as RepositoryInterface as VersionHistoryRepository, viewMock as RepositoryInterface as ViewRepository, ); return { sut, mocks: { access: accessMock, apiKey: apiKeyMock, cron: cronMock, crypto: cryptoMock, activity: activityMock, audit: auditMock, album: albumMock, albumUser: albumUserMock, asset: assetMock, config: configMock, database: databaseMock, event: eventMock, job: jobMock, library: libraryMock, logger: loggerMock, machineLearning: machineLearningMock, map: mapMock, media: mediaMock, memory: memoryMock, metadata: metadataMock, move: moveMock, notification: notificationMock, oauth: oauthMock, partner: partnerMock, person: personMock, process: processMock, search: searchMock, serverInfo: serverInfoMock, session: sessionMock, sharedLink: sharedLinkMock, stack: stackMock, storage: storageMock, systemMetadata: systemMock, tag: tagMock, telemetry: telemetryMock, trash: trashMock, user: userMock, versionHistory: versionHistoryMock, view: viewMock, } as ServiceMocks, }; }; const createPNG = (r: number, g: number, b: number) => { const image = new PNG({ width: 1, height: 1 }); image.data[0] = r; image.data[1] = g; image.data[2] = b; image.data[3] = 255; return PNG.sync.write(image); }; function* newPngFactory() { for (let r = 0; r < 255; r++) { for (let g = 0; g < 255; g++) { for (let b = 0; b < 255; b++) { yield createPNG(r, g, b); } } } } const pngFactory = newPngFactory(); export const getKyselyDB = async (suffix?: string): Promise> => { const parsed = parse(process.env.IMMICH_TEST_POSTGRES_URL!); const parsedOptions = { ...parsed, ssl: false, host: parsed.host ?? undefined, port: parsed.port ? Number(parsed.port) : undefined, database: parsed.database ?? undefined, }; const driverOptions = { ...parsedOptions, onnotice: (notice: Notice) => { if (notice['severity'] !== 'NOTICE') { console.warn('Postgres notice:', notice); } }, max: 10, types: { date: { to: 1184, from: [1082, 1114, 1184], serialize: (x: Date | string) => (x instanceof Date ? x.toISOString() : x), parse: (x: string) => new Date(x), }, bigint: { to: 20, from: [20], parse: (value: string) => Number.parseInt(value), serialize: (value: number) => value.toString(), }, }, connection: { TimeZone: 'UTC', }, }; const kysely = new Kysely({ dialect: new PostgresJSDialect({ postgres: postgres({ ...driverOptions, max: 1, database: 'postgres' }) }), }); const randomSuffix = Math.random().toString(36).slice(2, 7); const dbName = `immich_${suffix ?? randomSuffix}`; await sql.raw(`CREATE DATABASE ${dbName} WITH TEMPLATE immich OWNER postgres;`).execute(kysely); return new Kysely({ dialect: new PostgresJSDialect({ postgres: postgres({ ...driverOptions, database: dbName }) }), }); }; export const newRandomImage = () => { const { value } = pngFactory.next(); if (!value) { throw new Error('Ran out of random asset data'); } return value; }; export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: string, error?: unknown) => { return { stdout: new Readable({ read() { this.push(stdout); // write mock data to stdout this.push(null); // end stream }, }), stderr: new Readable({ read() { this.push(stderr); // write mock data to stderr this.push(null); // end stream }, }), stdin: new Writable({ write(chunk, encoding, callback) { callback(); }, }), exitCode, on: vitest.fn((event, callback: any) => { if (event === 'close') { callback(0); } if (event === 'error' && error) { callback(error); } if (event === 'exit') { callback(exitCode); } }), } as unknown as ChildProcessWithoutNullStreams; }); export async function* makeStream(items: T[] = []): AsyncIterableIterator { for (const item of items) { await Promise.resolve(); yield item; } }