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 { 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<typeof BaseService>; type Constructor<Type, Args extends Array<any>> = { new (...deps: Args): Type; }; type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface<AccessRepository[K]> }; export type ServiceMocks = { access: IAccessRepositoryMock; activity: Mocked<RepositoryInterface<ActivityRepository>>; album: Mocked<RepositoryInterface<AlbumRepository>>; albumUser: Mocked<RepositoryInterface<AlbumUserRepository>>; apiKey: Mocked<RepositoryInterface<ApiKeyRepository>>; audit: Mocked<RepositoryInterface<AuditRepository>>; asset: Mocked<RepositoryInterface<AssetRepository>>; config: Mocked<RepositoryInterface<ConfigRepository>>; cron: Mocked<RepositoryInterface<CronRepository>>; crypto: Mocked<RepositoryInterface<CryptoRepository>>; database: Mocked<RepositoryInterface<DatabaseRepository>>; event: Mocked<RepositoryInterface<EventRepository>>; job: Mocked<RepositoryInterface<JobRepository>>; library: Mocked<RepositoryInterface<LibraryRepository>>; logger: Mocked<RepositoryInterface<LoggingRepository>>; machineLearning: Mocked<RepositoryInterface<MachineLearningRepository>>; map: Mocked<RepositoryInterface<MapRepository>>; media: Mocked<RepositoryInterface<MediaRepository>>; memory: Mocked<RepositoryInterface<MemoryRepository>>; metadata: Mocked<RepositoryInterface<MetadataRepository>>; move: Mocked<RepositoryInterface<MoveRepository>>; notification: Mocked<RepositoryInterface<NotificationRepository>>; oauth: Mocked<RepositoryInterface<OAuthRepository>>; partner: Mocked<RepositoryInterface<PartnerRepository>>; person: Mocked<RepositoryInterface<PersonRepository>>; process: Mocked<RepositoryInterface<ProcessRepository>>; search: Mocked<RepositoryInterface<SearchRepository>>; serverInfo: Mocked<RepositoryInterface<ServerInfoRepository>>; session: Mocked<RepositoryInterface<SessionRepository>>; sharedLink: Mocked<RepositoryInterface<SharedLinkRepository>>; stack: Mocked<RepositoryInterface<StackRepository>>; storage: Mocked<RepositoryInterface<StorageRepository>>; systemMetadata: Mocked<RepositoryInterface<SystemMetadataRepository>>; tag: Mocked<RepositoryInterface<TagRepository>>; telemetry: ITelemetryRepositoryMock; trash: Mocked<RepositoryInterface<TrashRepository>>; user: Mocked<RepositoryInterface<UserRepository>>; versionHistory: Mocked<RepositoryInterface<VersionHistoryRepository>>; view: Mocked<RepositoryInterface<ViewRepository>>; }; export const newTestService = <T extends BaseService>( Service: Constructor<T, BaseServiceArgs>, 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<MetadataRepository> >; 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<RepositoryInterface<SyncRepository>>; const systemMock = newSystemMetadataRepositoryMock(); const tagMock = newTagRepositoryMock(); const telemetryMock = newTelemetryRepositoryMock(); const trashMock = newTrashRepositoryMock(); const userMock = (userRepository || newUserRepositoryMock()) as Mocked<RepositoryInterface<UserRepository>>; const versionHistoryMock = newVersionHistoryRepositoryMock(); const viewMock = newViewRepositoryMock(); const sut = new Service( loggerMock as RepositoryInterface<LoggingRepository> as LoggingRepository, accessMock as IAccessRepository as AccessRepository, activityMock as RepositoryInterface<ActivityRepository> as ActivityRepository, auditMock as RepositoryInterface<AuditRepository> as AuditRepository, albumMock as RepositoryInterface<AlbumRepository> as AlbumRepository, albumUserMock as RepositoryInterface<AlbumUserRepository> as AlbumUserRepository, assetMock as RepositoryInterface<AssetRepository> as AssetRepository, configMock as RepositoryInterface<ConfigRepository> as ConfigRepository, cronMock as RepositoryInterface<CronRepository> as CronRepository, cryptoMock as RepositoryInterface<CryptoRepository> as CryptoRepository, databaseMock as RepositoryInterface<DatabaseRepository> as DatabaseRepository, eventMock as RepositoryInterface<EventRepository> as EventRepository, jobMock as RepositoryInterface<JobRepository> as JobRepository, apiKeyMock as RepositoryInterface<ApiKeyRepository> as ApiKeyRepository, libraryMock as RepositoryInterface<LibraryRepository> as LibraryRepository, machineLearningMock as RepositoryInterface<MachineLearningRepository> as MachineLearningRepository, mapMock as RepositoryInterface<MapRepository> as MapRepository, mediaMock as RepositoryInterface<MediaRepository> as MediaRepository, memoryMock as RepositoryInterface<MemoryRepository> as MemoryRepository, metadataMock as RepositoryInterface<MetadataRepository> as MetadataRepository, moveMock as RepositoryInterface<MoveRepository> as MoveRepository, notificationMock as RepositoryInterface<NotificationRepository> as NotificationRepository, oauthMock as RepositoryInterface<OAuthRepository> as OAuthRepository, partnerMock as RepositoryInterface<PartnerRepository> as PartnerRepository, personMock as RepositoryInterface<PersonRepository> as PersonRepository, processMock as RepositoryInterface<ProcessRepository> as ProcessRepository, searchMock as RepositoryInterface<SearchRepository> as SearchRepository, serverInfoMock as RepositoryInterface<ServerInfoRepository> as ServerInfoRepository, sessionMock as RepositoryInterface<SessionRepository> as SessionRepository, sharedLinkMock as RepositoryInterface<SharedLinkRepository> as SharedLinkRepository, stackMock as RepositoryInterface<StackRepository> as StackRepository, storageMock as RepositoryInterface<StorageRepository> as StorageRepository, syncMock as RepositoryInterface<SyncRepository> as SyncRepository, systemMock as RepositoryInterface<SystemMetadataRepository> as SystemMetadataRepository, tagMock as RepositoryInterface<TagRepository> as TagRepository, telemetryMock as unknown as TelemetryRepository, trashMock as RepositoryInterface<TrashRepository> as TrashRepository, userMock as RepositoryInterface<UserRepository> as UserRepository, versionHistoryMock as RepositoryInterface<VersionHistoryRepository> as VersionHistoryRepository, viewMock as RepositoryInterface<ViewRepository> 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<Kysely<DB>> => { 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<DB>({ 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<DB>({ 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<T>(items: T[] = []): AsyncIterableIterator<T> { for (const item of items) { await Promise.resolve(); yield item; } }