fix: medium tests dependencies (#30612)

This commit is contained in:
Daniel Dietzler 2026-08-07 11:36:20 +02:00 committed by GitHub
parent a9a99cffbf
commit 5ad1e4e0f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 45 additions and 35 deletions

View file

@ -117,7 +117,7 @@ export const BASE_SERVICE_DEPENDENCIES = [
ViewRepository,
WebsocketRepository,
WorkflowRepository,
];
] as const;
@Injectable()
export class BaseService {
@ -191,7 +191,7 @@ export class BaseService {
);
}
static create<T extends BaseService>(Service: ClassConstructor<T>, ctx: BaseService) {
static create<T extends ClassConstructor<typeof BaseService>>(Service: T, ctx: BaseService) {
const service = new Service(
LoggingRepository.create(),
ctx.accessRepository,
@ -242,6 +242,7 @@ export class BaseService {
ctx.trashRepository,
ctx.userRepository,
ctx.versionRepository,
ctx.videoStreamRepository,
ctx.viewRepository,
ctx.websocketRepository,
ctx.workflowRepository,
@ -249,7 +250,7 @@ export class BaseService {
service.logger.setContext(BaseService.name);
return service as T;
return service as InstanceType<T>;
}
get worker() {

View file

@ -32,6 +32,7 @@ import {
UserMetadataKey,
WorkflowType,
} from 'src/enum';
import { Mocked } from 'vitest';
export type DeepPartial<T> = T extends Date
? T
@ -647,7 +648,10 @@ export type JSONSchemaProperty = {
required?: string[];
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export interface ClassConstructor<T = any> extends Function {
new (...args: any[]): T;
}
export type ClassConstructor<T> = T extends new (...args: infer R) => infer L
? new (...args: R) => L
: new (...args: any[]) => unknown;
export type ClassConstructorsToInstances<T extends readonly ClassConstructor<unknown>[]> = {
[K in keyof T]: InstanceType<T[K]> | Mocked<InstanceType<T[K]>>;
};

View file

@ -76,7 +76,7 @@ import { UserTable } from 'src/schema/tables/user.table';
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
import { MetadataService } from 'src/services/metadata.service';
import { SyncService } from 'src/services/sync.service';
import { ClassConstructor, UploadFile } from 'src/types';
import { ClassConstructor, ClassConstructorsToInstances, UploadFile } from 'src/types';
import { getConfig, updateConfig } from 'src/utils/config';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
@ -88,29 +88,34 @@ import { Mocked } from 'vitest';
export const testAssetsDir = resolve(__dirname, '../../e2e/test-assets');
type MediumTestOptions = {
mock: ClassConstructor<any>[];
real: ClassConstructor<any>[];
mock: Array<(typeof BASE_SERVICE_DEPENDENCIES)[number]>;
real: Array<(typeof BASE_SERVICE_DEPENDENCIES)[number]>;
database: Kysely<DB>;
};
export const newMediumService = <S extends BaseService>(Service: ClassConstructor<S>, options: MediumTestOptions) => {
type BaseServiceDeps = typeof BASE_SERVICE_DEPENDENCIES;
export const newMediumService = <S extends ClassConstructor<typeof BaseService>>(
Service: S,
options: MediumTestOptions,
) => {
const ctx = new MediumTestContext(Service, options);
return { sut: ctx.sut, ctx };
};
export class MediumTestContext<S extends BaseService = BaseService> {
export class MediumTestContext<S extends ClassConstructor<typeof BaseService> = ClassConstructor<typeof BaseService>> {
private repoCache: Record<string, any> = {};
private sutDeps: any[];
private sutDeps: ClassConstructorsToInstances<BaseServiceDeps>;
sut: S;
sut: InstanceType<S>;
database: Kysely<DB>;
constructor(
Service: ClassConstructor<S>,
Service: S,
private options: MediumTestOptions,
) {
this.sutDeps = this.makeDeps(options);
this.sut = new Service(...this.sutDeps);
this.sut = new Service(...this.sutDeps) as InstanceType<S>;
this.database = options.database;
}
@ -128,7 +133,7 @@ export class MediumTestContext<S extends BaseService = BaseService> {
throw new Error(`Real repository ${dep.name} is not a valid dependency`);
}
}
return (deps as ClassConstructor<any>[]).map((dep) => {
return deps.map((dep) => {
if (options.real.includes(dep)) {
return this.get(dep);
}
@ -136,10 +141,10 @@ export class MediumTestContext<S extends BaseService = BaseService> {
if (options.mock.includes(dep)) {
return newMockRepository(dep);
}
});
}) as unknown as ClassConstructorsToInstances<BaseServiceDeps>;
}
get<T>(key: ClassConstructor<T>): T {
get<T extends BaseServiceDeps[number]>(key: T): InstanceType<T> {
if (!Object.hasOwn(this.repoCache, key.name)) {
const real = newRealRepository(key, this.options.database);
this.repoCache[key.name] = real;
@ -148,8 +153,8 @@ export class MediumTestContext<S extends BaseService = BaseService> {
return this.repoCache[key.name];
}
getMock<T, R = Mocked<T>>(key: ClassConstructor<T>): R {
const index = BASE_SERVICE_DEPENDENCIES.indexOf(key as any);
getMock<T extends BaseServiceDeps[number], R = Mocked<InstanceType<T>>>(key: T): R {
const index = BASE_SERVICE_DEPENDENCIES.indexOf(key);
if (index === -1 || !this.options.mock.includes(key)) {
throw new Error(`getMock called with a key that is not a mock: ${key.name}`);
}
@ -328,7 +333,7 @@ export class MediumTestContext<S extends BaseService = BaseService> {
}
}
export class SyncTestContext extends MediumTestContext<SyncService> {
export class SyncTestContext extends MediumTestContext<typeof SyncService> {
constructor(database: Kysely<DB>) {
super(SyncService, {
database,
@ -379,7 +384,7 @@ const mockStats = {
birthtimeMs: 0,
};
export class ExifTestContext extends MediumTestContext<MetadataService> {
export class ExifTestContext extends MediumTestContext<typeof MetadataService> {
constructor(database: Kysely<DB>) {
super(MetadataService, {
database,
@ -431,7 +436,7 @@ export class ExifTestContext extends MediumTestContext<MetadataService> {
}
}
const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
const newRealRepository = <T extends BaseServiceDeps[number]>(key: T, db: Kysely<DB>): InstanceType<T> => {
switch (key) {
case AccessRepository:
case AlbumRepository:
@ -457,41 +462,41 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
case UserRepository:
case VersionHistoryRepository:
case WorkflowRepository: {
return new key(db);
return new key(db) as InstanceType<T>;
}
case ConfigRepository:
case CryptoRepository: {
return new key();
return new key() as InstanceType<T>;
}
case DatabaseRepository: {
return new key(db, LoggingRepository.create(), new ConfigRepository());
return new key(db, LoggingRepository.create(), new ConfigRepository()) as InstanceType<T>;
}
case EmailRepository: {
return new key(LoggingRepository.create());
return new key(LoggingRepository.create()) as InstanceType<T>;
}
case MediaRepository:
case MetadataRepository: {
return new key(LoggingRepository.create());
return new key(LoggingRepository.create()) as InstanceType<T>;
}
case PluginRepository: {
return new key(db, LoggingRepository.create());
return new key(db, LoggingRepository.create()) as InstanceType<T>;
}
case StorageRepository: {
return new key(LoggingRepository.create());
return new key(LoggingRepository.create()) as InstanceType<T>;
}
case TagRepository: {
return new key(db, LoggingRepository.create());
return new key(db, LoggingRepository.create()) as InstanceType<T>;
}
case LoggingRepository as unknown as ClassConstructor<LoggingRepository>: {
return new key() as unknown as T;
case LoggingRepository: {
return new key(undefined, undefined) as InstanceType<T>;
}
default: {

View file

@ -24,7 +24,7 @@ import { getKyselyDB } from 'test/utils';
let isInitialized = false;
class WorkflowTestContext extends MediumTestContext<WorkflowExecutionService> {
class WorkflowTestContext extends MediumTestContext<typeof WorkflowExecutionService> {
constructor(database: Kysely<DB>) {
super(WorkflowExecutionService, {
database,