import type { ModuleMetadata } from '@nestjs/common/interfaces/modules/module-metadata.interface'; import type { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface'; import type { TestingModuleBuilder } from '@nestjs/testing'; import { Test } from '@nestjs/testing'; import type { MockFactory } from '@nestjs/testing/interfaces'; import type { OverrideByFactoryOptions } from '@nestjs/testing/interfaces/override-by-factory-options.interface'; import type { PlatformOptions } from '@rsdk/core'; import { PlatformContext } from '@rsdk/core'; import { PlatformTestingModule } from './platform-testing-module'; type PlatformTestingOptions = Omit & { appName?: string; }; /** * Переопределение нужно ради того чтобы `builder.overridePipe(SomePipe).useValue(MockPipe)` * возвращал наш интерфейс */ export interface OverrideBy { useValue: (value: any) => PlatformTestingModuleBuilder; useFactory: ( options: OverrideByFactoryOptions, ) => PlatformTestingModuleBuilder; useClass: (metatype: any) => PlatformTestingModuleBuilder; } /** * Приходится переопределять каждый метод отдельно */ interface PlatformTestingModuleBuilder /** * Omit чтобы не ругался что типы не совпадают */ extends Omit< TestingModuleBuilder, | 'compile' | 'overrideProvider' | 'overridePipe' | 'overrideGuard' | 'overrideInterceptor' | 'overrideFilter' > { overridePipe(typeOrToken: T): OverrideBy; useMocker(mocker: MockFactory): TestingModuleBuilder; overrideFilter(typeOrToken: T): OverrideBy; overrideGuard(typeOrToken: T): OverrideBy; overrideInterceptor(typeOrToken: T): OverrideBy; overrideProvider(typeOrToken: T): OverrideBy; compile( options?: Pick, ): Promise; } export class PlatformTest { private constructor(private readonly options: PlatformTestingOptions) {} /** * Почему нужно использовать инстанс? * По большей степени исключительно для прозрачного разделения `API`. * Сначала определяем всю необходимую конфигурацию для платформы, после этого создаём тестируемый модуль * @param options */ static create(options: PlatformTestingOptions): PlatformTest { return new PlatformTest(options); } /** * Аналог метода `Test.createTestingModule` из nestjs * Ключевым отличием является то, что в возвращаемом билдере и производных недоступны методы * вызов которых может привести к неожиданному поведению * @param metadata */ createTestingModule(metadata: ModuleMetadata): PlatformTestingModuleBuilder { const contextOptions: PlatformOptions = { ...this.options, modules: [ { ...metadata, module: class TestingModule {}, }, ], }; const forceAppName = this.options.appName; const context = new PlatformContext(contextOptions, { skipAssertAppName: true, ...(forceAppName !== undefined && { forceAppName: forceAppName }), }); const resultMetadata = { imports: [context.getRoot()], }; /** * Создаём `testingModule` стандартными средствами nestjs */ const testingModule = Test.createTestingModule(resultMetadata); /** * Патчим метод `compile` для соответ */ // eslint-disable-next-line @typescript-eslint/unbound-method const originalCompile = testingModule.compile; Object.defineProperty(testingModule, 'compile', { value: async function ( ...args: Parameters ): Promise { const compiled = await Reflect.apply(originalCompile, this, args); return new PlatformTestingModule(compiled, context); }, }); // eslint-disable-next-line @typescript-eslint/unbound-method Object.defineProperty(testingModule.compile, 'name', { value: originalCompile.name, writable: false, }); return testingModule as unknown as PlatformTestingModuleBuilder; } }