chore: resolve conflicts

This commit is contained in:
Kamil Myśliwiec
2023-04-17 13:49:01 +02:00
22 changed files with 750 additions and 44 deletions

View File

@@ -0,0 +1,12 @@
import { Injectable, Module, forwardRef } from '@nestjs/common';
import { BModule, BProvider } from './b.module';
@Injectable()
export class AProvider {}
@Module({
imports: [forwardRef(() => BModule)],
providers: [AProvider],
exports: [AProvider],
})
export class AModule {}

View File

@@ -0,0 +1,12 @@
import { Injectable, Module, forwardRef } from '@nestjs/common';
import { AModule, AProvider } from './a.module';
@Injectable()
export class BProvider {}
@Module({
imports: [forwardRef(() => AModule)],
providers: [BProvider],
exports: [BProvider],
})
export class BModule {}

View File

@@ -0,0 +1,309 @@
import {
Controller,
DynamicModule,
forwardRef,
Global,
Injectable,
Module,
} from '@nestjs/common';
import { LazyModuleLoader } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { expect } from 'chai';
import { AModule, AProvider } from './circular-dependency/a.module';
import { BModule, BProvider } from './circular-dependency/b.module';
describe('Modules overriding', () => {
describe('Top-level module', () => {
@Controller()
class ControllerOverwritten {}
@Module({
controllers: [ControllerOverwritten],
})
class ModuleToBeOverwritten {}
@Controller()
class ControllerOverride {}
@Module({
controllers: [ControllerOverride],
})
class ModuleOverride {}
let testingModule: TestingModule;
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [ModuleToBeOverwritten],
})
.overrideModule(ModuleToBeOverwritten)
.useModule(ModuleOverride)
.compile();
});
it('should override top-level modules using testing module builder', () => {
expect(() =>
testingModule.get<ControllerOverwritten>(ControllerOverwritten),
).to.throw();
expect(
testingModule.get<ControllerOverride>(ControllerOverride),
).to.be.an.instanceof(ControllerOverride);
});
});
describe('Dynamic module', () => {
@Controller()
class ControllerOverwritten {}
@Module({})
class DynamicModuleToBeOverwritten {}
const dynamicModuleOverwritten: DynamicModule = {
module: DynamicModuleToBeOverwritten,
controllers: [ControllerOverwritten],
};
@Controller()
class ControllerOverride {}
@Module({})
class DynamicModuleOverride {}
const dynamicModuleOverride: DynamicModule = {
module: DynamicModuleOverride,
controllers: [ControllerOverride],
};
let testingModule: TestingModule;
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [dynamicModuleOverwritten],
})
.overrideModule(dynamicModuleOverwritten)
.useModule(dynamicModuleOverride)
.compile();
});
it('should override dynamic modules using testing module builder', () => {
expect(() =>
testingModule.get<ControllerOverwritten>(ControllerOverwritten),
).to.throw();
expect(
testingModule.get<ControllerOverride>(ControllerOverride),
).to.be.an.instanceof(ControllerOverride);
});
});
describe('Circular dependency module', () => {
let testingModule: TestingModule;
@Injectable()
class CProvider {}
@Module({
providers: [CProvider],
})
class CModule {}
@Injectable()
class BProviderOverride {}
@Module({
imports: [forwardRef(() => AModule), forwardRef(() => CModule)],
providers: [BProviderOverride],
exports: [BProviderOverride],
})
class BModuleOverride {}
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [AModule],
})
.overrideModule(BModule)
.useModule(BModuleOverride)
.compile();
});
it('should override top-level modules using testing module builder', () => {
expect(testingModule.get<AProvider>(AProvider)).to.be.an.instanceof(
AProvider,
);
expect(() => testingModule.get<BProvider>(BProvider)).to.throw();
expect(testingModule.get<CProvider>(CProvider)).to.be.an.instanceof(
CProvider,
);
expect(
testingModule.get<BProviderOverride>(BProviderOverride),
).to.be.an.instanceof(BProviderOverride);
});
});
describe('Nested module', () => {
let testingModule: TestingModule;
@Controller()
class OverwrittenNestedModuleController {}
@Module({
controllers: [OverwrittenNestedModuleController],
})
class OverwrittenNestedModule {}
@Controller()
class OverrideNestedModuleController {}
@Module({
controllers: [OverrideNestedModuleController],
})
class OverrideNestedModule {}
@Module({
imports: [OverwrittenNestedModule],
})
class AppModule {}
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(OverwrittenNestedModule)
.useModule(OverrideNestedModule)
.compile();
});
it('should override nested modules using testing module builder', () => {
expect(
testingModule.get<OverrideNestedModuleController>(
OverrideNestedModuleController,
),
).to.be.an.instanceof(OverrideNestedModuleController);
expect(() =>
testingModule.get<OverwrittenNestedModuleController>(
OverwrittenNestedModuleController,
),
).to.throw();
});
});
describe('Lazy-loaded module', () => {
let testingModule: TestingModule;
@Injectable()
class OverwrittenLazyProvider {
value() {
return 'overwritten lazy';
}
}
@Module({
providers: [
{
provide: 'LAZY_PROVIDER',
useClass: OverwrittenLazyProvider,
},
],
})
class OverwrittenLazyModule {}
@Injectable()
class OverrideLazyProvider {
value() {
return 'override lazy';
}
}
@Module({
providers: [
{
provide: 'LAZY_PROVIDER',
useClass: OverrideLazyProvider,
},
],
})
class OverrideLazyModule {}
@Injectable()
class AppService {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async value() {
const moduleRef = await this.lazyModuleLoader.load(
() => OverwrittenLazyModule,
);
return moduleRef.get('LAZY_PROVIDER').value();
}
}
@Module({
imports: [],
providers: [AppService],
})
class AppModule {}
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(OverwrittenLazyModule)
.useModule(OverrideLazyModule)
.compile();
});
it('should override lazy loaded modules using testing module builder', async () => {
const result = await testingModule.get<AppService>(AppService).value();
expect(result).to.be.equal('override lazy');
});
});
describe('Global module', () => {
let testingModule: TestingModule;
@Injectable()
class OverwrittenProvider {
value() {
return 'overwritten lazy';
}
}
@Global()
@Module({
providers: [OverwrittenProvider],
exports: [OverwrittenProvider],
})
class OverwrittenModule {}
@Injectable()
class OverrideProvider {
value() {
return 'override lazy';
}
}
@Global()
@Module({
providers: [OverrideProvider],
exports: [OverrideProvider],
})
class OverrideModule {}
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [OverwrittenModule],
})
.overrideModule(OverwrittenModule)
.useModule(OverrideModule)
.compile();
});
it('should override global modules using testing module builder', () => {
expect(
testingModule.get<OverrideProvider>(OverrideProvider),
).to.be.an.instanceof(OverrideProvider);
expect(() =>
testingModule.get<OverwrittenProvider>(OverwrittenProvider),
).to.throw();
});
});
});

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}

View File

@@ -13,7 +13,7 @@ import {
import { InitializeOnPreviewAllowlist } from '../inspector/initialize-on-preview.allowlist';
import { SerializedGraph } from '../inspector/serialized-graph';
import { REQUEST } from '../router/request/request-constants';
import { ModuleCompiler } from './compiler';
import { ModuleCompiler, ModuleFactory } from './compiler';
import { ContextId } from './instance-wrapper';
import { InternalCoreModule } from './internal-core-module/internal-core-module';
import { InternalProvidersStorage } from './internal-providers-storage';
@@ -21,6 +21,9 @@ import { Module } from './module';
import { ModuleTokenFactory } from './module-token-factory';
import { ModulesContainer } from './modules-container';
type ModuleMetatype = Type<any> | DynamicModule | Promise<DynamicModule>;
type ModuleScope = Type<any>[];
export class NestContainer {
private readonly globalModules = new Set<Module>();
private readonly moduleTokenFactory = new ModuleTokenFactory();
@@ -65,8 +68,8 @@ export class NestContainer {
}
public async addModule(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
scope: Type<any>[],
metatype: ModuleMetatype,
scope: ModuleScope,
): Promise<Module | undefined> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
@@ -79,6 +82,47 @@ export class NestContainer {
if (this.modules.has(token)) {
return this.modules.get(token);
}
return this.setModule(
{
token,
type,
dynamicMetadata,
},
scope,
);
}
public async replaceModule(
metatypeToReplace: ModuleMetatype,
newMetatype: ModuleMetatype,
scope: ModuleScope,
): Promise<Module | undefined> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
if (!metatypeToReplace || !newMetatype) {
throw new UndefinedForwardRefException(scope);
}
const { token } = await this.moduleCompiler.compile(metatypeToReplace);
const { type, dynamicMetadata } = await this.moduleCompiler.compile(
newMetatype,
);
return this.setModule(
{
token,
type,
dynamicMetadata,
},
scope,
);
}
private async setModule(
{ token, dynamicMetadata, type }: ModuleFactory,
scope: ModuleScope,
): Promise<Module | undefined> {
const moduleRef = new Module(type, this);
moduleRef.token = token;
moduleRef.initOnPreview = this.shouldInitOnPreview(type);
@@ -91,6 +135,7 @@ export class NestContainer {
moduleRef.isGlobal = true;
this.addGlobalModule(moduleRef);
}
return moduleRef;
}

View File

@@ -3,6 +3,7 @@ import { ExternalContextCreator } from '../../helpers/external-context-creator';
import { HttpAdapterHost } from '../../helpers/http-adapter-host';
import { GraphInspector } from '../../inspector/graph-inspector';
import { SerializedGraph } from '../../inspector/serialized-graph';
import { ModuleOverride } from '../../interfaces/module-override.interface';
import { DependenciesScanner } from '../../scanner';
import { ModuleCompiler } from '../compiler';
import { NestContainer } from '../container';
@@ -19,6 +20,7 @@ export class InternalCoreModuleFactory {
moduleCompiler: ModuleCompiler,
httpAdapterHost: HttpAdapterHost,
graphInspector: GraphInspector,
moduleOverrides?: ModuleOverride[],
) {
const lazyModuleLoaderFactory = () => {
const logger = new Logger(LazyModuleLoader.name, {
@@ -36,6 +38,7 @@ export class InternalCoreModuleFactory {
instanceLoader,
moduleCompiler,
container.getModules(),
moduleOverrides,
);
};

View File

@@ -1,4 +1,5 @@
import { DynamicModule, Type } from '@nestjs/common';
import { ModuleOverride } from '../../interfaces/module-override.interface';
import { DependenciesScanner } from '../../scanner';
import { ModuleCompiler } from '../compiler';
import { SilentLogger } from '../helpers/silent-logger';
@@ -14,6 +15,7 @@ export class LazyModuleLoader {
private readonly instanceLoader: InstanceLoader,
private readonly moduleCompiler: ModuleCompiler,
private readonly modulesContainer: ModulesContainer,
private readonly moduleOverrides?: ModuleOverride[],
) {}
public async load(
@@ -26,9 +28,10 @@ export class LazyModuleLoader {
this.registerLoggerConfiguration(loadOpts);
const moduleClassOrDynamicDefinition = await loaderFn();
const moduleInstances = await this.dependenciesScanner.scanForModules(
moduleClassOrDynamicDefinition,
);
const moduleInstances = await this.dependenciesScanner.scanForModules({
moduleDefinition: moduleClassOrDynamicDefinition,
overrides: this.moduleOverrides,
});
if (moduleInstances.length === 0) {
// The module has been loaded already. In this case, we must
// retrieve a module reference from the existing container.

View File

@@ -0,0 +1,8 @@
import { DynamicModule, ForwardReference } from '@nestjs/common';
import { Type } from '@nestjs/common/interfaces';
export type ModuleDefinition =
| ForwardReference
| Type<unknown>
| DynamicModule
| Promise<DynamicModule>;

View File

@@ -0,0 +1,6 @@
import { ModuleDefinition } from './module-definition.interface';
export interface ModuleOverride {
moduleToReplace: ModuleDefinition;
newModule: ModuleDefinition;
}

View File

@@ -52,6 +52,8 @@ import { InternalCoreModuleFactory } from './injector/internal-core-module/inter
import { Module } from './injector/module';
import { GraphInspector } from './inspector/graph-inspector';
import { UuidFactory } from './inspector/uuid-factory';
import { ModuleDefinition } from './interfaces/module-definition.interface';
import { ModuleOverride } from './interfaces/module-override.interface';
import { MetadataScanner } from './metadata-scanner';
interface ApplicationProviderWrapper {
@@ -61,6 +63,13 @@ interface ApplicationProviderWrapper {
scope?: Scope;
}
interface ModulesScanParameters {
moduleDefinition: ModuleDefinition;
scope?: Type<unknown>[];
ctxRegistry?: (ForwardReference | DynamicModule | Type<unknown>)[];
overrides?: ModuleOverride[];
}
export class DependenciesScanner {
private readonly applicationProvidersApplyMap: ApplicationProviderWrapper[] =
[];
@@ -72,9 +81,15 @@ export class DependenciesScanner {
private readonly applicationConfig = new ApplicationConfig(),
) {}
public async scan(module: Type<any>) {
await this.registerCoreModule();
await this.scanForModules(module);
public async scan(
module: Type<any>,
options?: { overrides?: ModuleOverride[] },
) {
await this.registerCoreModule(options?.overrides);
await this.scanForModules({
moduleDefinition: module,
overrides: options?.overrides,
});
await this.scanModulesForDependencies();
this.calculateModulesDistance();
@@ -82,16 +97,21 @@ export class DependenciesScanner {
this.container.bindGlobalScope();
}
public async scanForModules(
moduleDefinition:
| ForwardReference
| Type<unknown>
| DynamicModule
| Promise<DynamicModule>,
scope: Type<unknown>[] = [],
ctxRegistry: (ForwardReference | DynamicModule | Type<unknown>)[] = [],
): Promise<Module[]> {
const moduleInstance = await this.insertModule(moduleDefinition, scope);
public async scanForModules({
moduleDefinition,
scope = [],
ctxRegistry = [],
overrides = [],
}: ModulesScanParameters): Promise<Module[]> {
const moduleInstance = await this.insertOrOverrideModule(
moduleDefinition,
overrides,
scope,
);
moduleDefinition =
this.getOverrideModuleByModule(moduleDefinition, overrides)?.newModule ??
moduleDefinition;
moduleDefinition =
moduleDefinition instanceof Promise
? await moduleDefinition
@@ -128,11 +148,12 @@ export class DependenciesScanner {
if (ctxRegistry.includes(innerModule)) {
continue;
}
const moduleRefs = await this.scanForModules(
innerModule,
[].concat(scope, moduleDefinition),
const moduleRefs = await this.scanForModules({
moduleDefinition: innerModule,
scope: [].concat(scope, moduleDefinition),
ctxRegistry,
);
overrides,
});
registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
}
if (!moduleInstance) {
@@ -496,6 +517,60 @@ export class DependenciesScanner {
this.container.addController(controller, token);
}
private insertOrOverrideModule(
moduleDefinition: ModuleDefinition,
overrides: ModuleOverride[],
scope: Type<unknown>[],
): Promise<Module | undefined> {
const overrideModule = this.getOverrideModuleByModule(
moduleDefinition,
overrides,
);
if (overrideModule !== undefined) {
return this.overrideModule(
moduleDefinition,
overrideModule.newModule,
scope,
);
}
return this.insertModule(moduleDefinition, scope);
}
private getOverrideModuleByModule(
module: ModuleDefinition,
overrides: ModuleOverride[],
): ModuleOverride | undefined {
if (this.isForwardReference(module)) {
return overrides.find(moduleToOverride => {
return (
moduleToOverride.moduleToReplace === module.forwardRef() ||
(
moduleToOverride.moduleToReplace as ForwardReference
).forwardRef?.() === module.forwardRef()
);
});
}
return overrides.find(
moduleToOverride => moduleToOverride.moduleToReplace === module,
);
}
private async overrideModule(
moduleToOverride: ModuleDefinition,
newModule: ModuleDefinition,
scope: Type<unknown>[],
): Promise<Module | undefined> {
return this.container.replaceModule(
this.isForwardReference(moduleToOverride)
? moduleToOverride.forwardRef()
: moduleToOverride,
this.isForwardReference(newModule) ? newModule.forwardRef() : newModule,
scope,
);
}
public reflectMetadata<T = any>(
metadataKey: string,
metatype: Type<any>,
@@ -503,15 +578,19 @@ export class DependenciesScanner {
return Reflect.getMetadata(metadataKey, metatype) || [];
}
public async registerCoreModule() {
public async registerCoreModule(overrides?: ModuleOverride[]) {
const moduleDefinition = InternalCoreModuleFactory.create(
this.container,
this,
this.container.getModuleCompiler(),
this.container.getHttpAdapterHostRef(),
this.graphInspector,
overrides,
);
const [instance] = await this.scanForModules(moduleDefinition);
const [instance] = await this.scanForModules({
moduleDefinition,
overrides,
});
this.container.registerCoreModuleRef(instance);
}
@@ -637,7 +716,7 @@ export class DependenciesScanner {
}
private isForwardReference(
module: Type<any> | DynamicModule | ForwardReference,
module: ModuleDefinition,
): module is ForwardReference {
return module && !!(module as ForwardReference).forwardRef;
}

View File

@@ -119,7 +119,7 @@ describe('ExceptionsHandler', () => {
handler.setCustomFilters(filters as any);
expect((handler as any).filters).to.be.eql(filters);
});
it('should throws exception when passed argument is not an array', () => {
it('should throw exception when passed argument is not an array', () => {
expect(() => handler.setCustomFilters(null)).to.throws(
InvalidExceptionFilterException,
);

View File

@@ -1,8 +1,8 @@
import { expect } from 'chai';
import { of } from 'rxjs';
import * as sinon from 'sinon';
import { ExternalExceptionsHandler } from '../../exceptions/external-exceptions-handler';
import { ExternalExceptionFilter } from '../../exceptions/external-exception-filter';
import { ExternalExceptionsHandler } from '../../exceptions/external-exceptions-handler';
describe('ExternalExceptionsHandler', () => {
let handler: ExternalExceptionsHandler;
@@ -37,7 +37,7 @@ describe('ExternalExceptionsHandler', () => {
handler.setCustomFilters(filters as any);
expect((handler as any).filters).to.be.eql(filters);
});
it('should throws exception when passed argument is not an array', () => {
it('should throw exception when passed argument is not an array', () => {
expect(() => handler.setCustomFilters(null)).to.throw();
});
});

View File

@@ -71,7 +71,7 @@ describe('NestContainer', () => {
expect(setSpy.calledOnce).to.be.true;
});
it('should throws an exception when metatype is not defined', () => {
it('should throw an exception when metatype is not defined', () => {
expect(container.addModule(undefined, [])).to.eventually.throws();
});
@@ -81,6 +81,37 @@ describe('NestContainer', () => {
expect(addGlobalModuleSpy.calledOnce).to.be.true;
});
});
describe('replaceModule', () => {
it('should replace module if already exists in collection', async () => {
@Module({})
class ReplaceTestModule {}
const modules = new Map();
const setSpy = sinon.spy(modules, 'set');
(container as any).modules = modules;
await container.addModule(TestModule as any, []);
await container.replaceModule(
TestModule as any,
ReplaceTestModule as any,
[],
);
expect(setSpy.calledTwice).to.be.true;
});
it('should throw an exception when metatype is not defined', () => {
expect(container.addModule(undefined, [])).to.eventually.throws();
});
it('should add global module when module is global', async () => {
const addGlobalModuleSpy = sinon.spy(container, 'addGlobalModule');
await container.addModule(GlobalTestModule as any, []);
expect(addGlobalModuleSpy.calledOnce).to.be.true;
});
});
describe('isGlobalModule', () => {
describe('when module is not globally scoped', () => {
it('should return false', () => {

View File

@@ -306,7 +306,7 @@ describe('Module', () => {
beforeEach(() => {
sinon.stub((module as any)._providers, 'has').returns(false);
});
it('should throws RuntimeException', () => {
it('should throw RuntimeException', () => {
expect(() => module.instance).to.throws(RuntimeException);
});
});

View File

@@ -14,6 +14,7 @@ import { UndefinedModuleException } from '../errors/exceptions/undefined-module.
import { NestContainer } from '../injector/container';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { GraphInspector } from '../inspector/graph-inspector';
import { ModuleOverride } from '../interfaces/module-override.interface';
import { MetadataScanner } from '../metadata-scanner';
import { DependenciesScanner } from '../scanner';
import Sinon = require('sinon');
@@ -77,11 +78,17 @@ describe('DependenciesScanner', () => {
mockContainer.restore();
});
it('should "insertModule" call twice (2 modules) container method "addModule"', async () => {
const expectation = mockContainer.expects('addModule').twice();
it('should "insertOrOverrideModule" call twice (2 modules) container method "addModule"', async () => {
const expectationCountAddModule = mockContainer
.expects('addModule')
.twice();
const expectationCountReplaceModule = mockContainer
.expects('replaceModule')
.never();
await scanner.scan(TestModule);
expectation.verify();
await scanner.scan(TestModule as any);
expectationCountAddModule.verify();
expectationCountReplaceModule.verify();
});
it('should "insertProvider" call twice (2 components) container method "addProvider"', async () => {
@@ -105,6 +112,138 @@ describe('DependenciesScanner', () => {
expectation.verify();
});
describe('when there is modules overrides', () => {
@Injectable()
class OverwrittenTestComponent {}
@Controller('')
class OverwrittenControlerOne {}
@Controller('')
class OverwrittenControllerTwo {}
@Module({
controllers: [OverwrittenControlerOne],
providers: [OverwrittenTestComponent],
})
class OverwrittenModuleOne {}
@Module({
controllers: [OverwrittenControllerTwo],
})
class OverwrittenModuleTwo {}
@Module({
imports: [OverwrittenModuleOne, OverwrittenModuleTwo],
})
class OverrideTestModule {}
@Injectable()
class OverrideTestComponent {}
@Controller('')
class OverrideControllerOne {}
@Controller('')
class OverrideControllerTwo {}
@Module({
controllers: [OverwrittenControlerOne],
providers: [OverrideTestComponent],
})
class OverrideModuleOne {}
@Module({
controllers: [OverrideControllerTwo],
})
class OverrideModuleTwo {}
const modulesToOverride: ModuleOverride[] = [
{ moduleToReplace: OverwrittenModuleOne, newModule: OverrideModuleOne },
{ moduleToReplace: OverwrittenModuleTwo, newModule: OverrideModuleTwo },
];
it('should "putModule" call twice (2 modules) container method "replaceModule"', async () => {
const expectationReplaceModuleFirst = mockContainer
.expects('replaceModule')
.once()
.withArgs(OverwrittenModuleOne, OverrideModuleOne, sinon.match.array);
const expectationReplaceModuleSecond = mockContainer
.expects('replaceModule')
.once()
.withArgs(OverwrittenModuleTwo, OverrideModuleTwo, sinon.match.array);
const expectationCountAddModule = mockContainer
.expects('addModule')
.once();
await scanner.scan(OverrideTestModule as any, {
overrides: modulesToOverride,
});
expectationReplaceModuleFirst.verify();
expectationReplaceModuleSecond.verify();
expectationCountAddModule.verify();
});
it('should "insertProvider" call once container method "addProvider"', async () => {
const expectation = mockContainer.expects('addProvider').once();
await scanner.scan(OverrideTestModule as any);
expectation.verify();
});
it('should "insertController" call twice (2 components) container method "addController"', async () => {
const expectation = mockContainer.expects('addController').twice();
await scanner.scan(OverrideTestModule as any);
expectation.verify();
});
it('should "putModule" call container method "replaceModule" with forwardRef() when forwardRef property exists', async () => {
const overwrittenForwardRefSpy = sinon.spy();
@Module({})
class OverwrittenForwardRef {}
@Module({})
class Overwritten {
public static forwardRef() {
overwrittenForwardRefSpy();
return OverwrittenForwardRef;
}
}
const overrideForwardRefSpy = sinon.spy();
@Module({})
class OverrideForwardRef {}
@Module({})
class Override {
public static forwardRef() {
overrideForwardRefSpy();
return OverrideForwardRef;
}
}
@Module({
imports: [Overwritten],
})
class OverrideForwardRefTestModule {}
await scanner.scan(OverrideForwardRefTestModule as any, {
overrides: [
{
moduleToReplace: Overwritten,
newModule: Override,
},
],
});
expect(overwrittenForwardRefSpy.called).to.be.true;
expect(overrideForwardRefSpy.called).to.be.true;
});
});
describe('reflectDynamicMetadata', () => {
describe('when param has prototype', () => {
it('should call "reflectParamInjectables" and "reflectInjectables"', () => {
@@ -595,14 +734,20 @@ describe('DependenciesScanner', () => {
describe('scanForModules', () => {
it('should throw an exception when the imports array includes undefined', () => {
try {
scanner.scanForModules(UndefinedModule, [UndefinedModule]);
scanner.scanForModules({
moduleDefinition: UndefinedModule,
scope: [UndefinedModule],
});
} catch (exception) {
expect(exception instanceof UndefinedModuleException).to.be.true;
}
});
it('should throw an exception when the imports array includes an invalid value', () => {
try {
scanner.scanForModules(InvalidModule, [InvalidModule]);
scanner.scanForModules({
moduleDefinition: InvalidModule,
scope: [InvalidModule],
});
} catch (exception) {
expect(exception instanceof InvalidModuleException).to.be.true;
}

View File

@@ -116,7 +116,7 @@ describe('RpcContextCreator', () => {
expect(tryActivateSpy.called).to.be.true;
});
describe('when can not activate', () => {
it('should throws forbidden exception', async () => {
it('should throw forbidden exception', async () => {
sinon
.stub(guardsConsumer, 'tryActivate')
.callsFake(async () => false);

View File

@@ -76,7 +76,7 @@ describe('RpcExceptionsHandler', () => {
handler.setCustomFilters(filters as any);
expect((handler as any).filters).to.be.eql(filters);
});
it('should throws exception when passed argument is not an array', () => {
it('should throw exception when passed argument is not an array', () => {
expect(() => handler.setCustomFilters(null)).to.throw();
});
});

View File

@@ -0,0 +1,9 @@
import { ModuleDefinition } from '../../core/interfaces/module-definition.interface';
import { TestingModuleBuilder } from '../testing-module.builder';
/**
* @publicApi
*/
export interface OverrideModule {
useModule: (newModule: ModuleDefinition) => TestingModuleBuilder;
}

View File

@@ -10,11 +10,14 @@ import {
} from '@nestjs/core/inspector/uuid-factory';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { DependenciesScanner } from '@nestjs/core/scanner';
import { ModuleDefinition } from '../core/interfaces/module-definition.interface';
import { ModuleOverride } from '../core/interfaces/module-override.interface';
import {
MockFactory,
OverrideBy,
OverrideByFactoryOptions,
} from './interfaces';
import { OverrideModule } from './interfaces/override-module.interface';
import { TestingLogger } from './services/testing-logger.service';
import { TestingInjector } from './testing-injector';
import { TestingInstanceLoader } from './testing-instance-loader';
@@ -27,6 +30,10 @@ export class TestingModuleBuilder {
private readonly applicationConfig = new ApplicationConfig();
private readonly container = new NestContainer(this.applicationConfig);
private readonly overloadsMap = new Map();
private readonly moduleOverloadsMap = new Map<
ModuleDefinition,
ModuleDefinition
>();
private readonly module: any;
private testingLogger: LoggerService;
private mocker?: MockFactory;
@@ -68,6 +75,15 @@ export class TestingModuleBuilder {
return this.override(typeOrToken, true);
}
public overrideModule(moduleToOverride: ModuleDefinition): OverrideModule {
return {
useModule: newModule => {
this.moduleOverloadsMap.set(moduleToOverride, newModule);
return this;
},
};
}
public async compile(
options: Pick<NestApplicationContextOptions, 'snapshot' | 'preview'> = {},
): Promise<TestingModule> {
@@ -88,7 +104,9 @@ export class TestingModuleBuilder {
graphInspector,
this.applicationConfig,
);
await scanner.scan(this.module);
await scanner.scan(this.module, {
overrides: this.getModuleOverloads(),
});
this.applyOverloadsMap();
await this.createInstancesOfDependencies(graphInspector, options);
@@ -126,11 +144,20 @@ export class TestingModuleBuilder {
}
private applyOverloadsMap() {
[...this.overloadsMap.entries()].forEach(([item, options]) => {
const overloads = [...this.overloadsMap.entries()];
overloads.forEach(([item, options]) => {
this.container.replace(item, options);
});
}
private getModuleOverloads(): ModuleOverride[] {
const overloads = [...this.moduleOverloadsMap.entries()];
return overloads.map(([moduleToReplace, newModule]) => ({
moduleToReplace,
newModule,
}));
}
private getRootModule() {
const modules = this.container.getModules().values();
return modules.next().value;

View File

@@ -113,7 +113,7 @@ describe('WsContextCreator', () => {
expect(tryActivateSpy.called).to.be.true;
});
describe('when can not activate', () => {
it('should throws forbidden exception', async () => {
it('should throw forbidden exception', async () => {
sinon
.stub(guardsConsumer, 'tryActivate')
.callsFake(async () => false);

View File

@@ -66,7 +66,7 @@ describe('WsExceptionsHandler', () => {
handler.setCustomFilters(filters as any);
expect((handler as any).filters).to.be.eql(filters);
});
it('should throws exception when passed argument is not an array', () => {
it('should throw exception when passed argument is not an array', () => {
expect(() => handler.setCustomFilters(null)).to.throw();
});
});

View File

@@ -72,7 +72,7 @@ describe('WebSocketsController', () => {
subscribeToServerEvents = sinon.spy();
(instance as any).subscribeToServerEvents = subscribeToServerEvents;
});
it('should throws "InvalidSocketPortException" when port is not a number', () => {
it('should throw "InvalidSocketPortException" when port is not a number', () => {
Reflect.defineMetadata(PORT_METADATA, 'test', InvalidGateway);
expect(() =>
instance.connectGatewayToServer(