update(@nestjs) increase test coverage, update package dependencies

This commit is contained in:
kamil.mysliwiec
2017-07-23 11:34:47 +02:00
parent b79eda21de
commit 260fd566ab
56 changed files with 1227 additions and 208 deletions

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ npm-debug.log
# example
/quick-start
/example_dist
/example
# tests
/test

View File

@@ -8,5 +8,4 @@ before_script:
- sh -e /etc/init.d/xvfb start
script:
- npm test
- npm run build
after_success: npm run coverage

View File

@@ -3,6 +3,6 @@ import { Guard, CanActivate } from '@nestjs/common';
@Guard()
export class RolesGuard implements CanActivate {
public async canActivate() {
return await Promise.resolve(false);
return await Promise.resolve(true);
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "nestjs",
"version": "3.0.0",
"version": "4.0.0",
"description": "Modern, fast, powerful node.js web framework",
"main": "index.js",
"scripts": {
@@ -48,6 +48,7 @@
"@nestjs/testing": "*",
"@nestjs/websockets": "*",
"cli-color": "^1.1.0",
"engine.io-client": "^3.1.1",
"express": "^4.14.0",
"iterare": "0.0.8",
"json-socket": "^0.2.1",
@@ -55,11 +56,12 @@
"redis": "^2.7.1",
"reflect-metadata": "^0.1.10",
"rxjs": "^5.4.2",
"socket.io": "^1.7.2",
"socket.io": "^1.7.4",
"typescript": "^2.4.1"
},
"devDependencies": {
"@types/chai": "^3.4.34",
"@types/chai": "^3.5.2",
"@types/chai-as-promised": "0.0.31",
"@types/express": "^4.0.35",
"@types/mocha": "^2.2.38",
"@types/node": "^7.0.5",
@@ -70,6 +72,7 @@
"awesome-typescript-loader": "^3.0.0-beta.18",
"body-parser": "^1.17.2",
"chai": "^3.5.0",
"chai-as-promised": "^7.1.1",
"concurrently": "^3.4.0",
"core-js": "^2.4.1",
"coveralls": "^2.11.16",

View File

@@ -3,8 +3,8 @@ import { INestMicroservice, ExceptionFilter, PipeTransform } from './index';
import { WebSocketAdapter } from './web-socket-adapter.interface';
export interface INestApplication {
init(): void;
listen(port: number, callback?: () => void);
init(): Promise<void>;
listen(port: number, callback?: () => void): Promise<any>;
setGlobalPrefix(prefix: string): void;
useWebSocketAdapter(adapter: WebSocketAdapter): void;
connectMicroservice(config: MicroserviceConfiguration): INestMicroservice;

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/common",
"version": "2.0.3",
"version": "4.0.0",
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
"author": "Kamil Mysliwiec",
"license": "MIT",

View File

@@ -0,0 +1,26 @@
import 'reflect-metadata';
import { expect } from 'chai';
import { UseGuards } from '../../utils/decorators/use-guards.decorator';
import { GUARDS_METADATA } from './../../constants';
describe('@UseGuards', () => {
const guards = [ 'guard1', 'guard2' ];
@UseGuards(...guards as any) class Test {}
class TestWithMethod {
@UseGuards(...guards as any)
public static test() {}
}
it('should enhance class with expected guards array', () => {
const metadata = Reflect.getMetadata(GUARDS_METADATA, Test);
expect(metadata).to.be.eql(guards);
});
it('should enhance method with expected guards array', () => {
const metadata = Reflect.getMetadata(GUARDS_METADATA, TestWithMethod.test);
expect(metadata).to.be.eql(guards);
});
});

View File

@@ -4,17 +4,12 @@ import { isUndefined, isFunction, isNil, isEmpty } from '@nestjs/common/utils/sh
import { Controller } from '@nestjs/common/interfaces';
import { CanActivate, HttpStatus } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/toPromise';
import { HttpException } from '../index';
import { FORBIDDEN_MESSAGE } from './constants';
import 'rxjs/add/operator/toPromise';
export class GuardsConsumer {
public async tryActivate(
guards: CanActivate[],
data: any,
instance: Controller,
callback: (...args) => any): Promise<boolean> {
public async tryActivate(guards: CanActivate[], data, instance: Controller, callback: (...args) => any): Promise<boolean> {
if (!guards || isEmpty(guards)) {
return true;
}

View File

@@ -101,7 +101,7 @@ export class Injector {
const instances = await Promise.all(args.map(async (param) => {
const paramWrapper = await this.resolveSingleParam<T>(wrapper, param, module, context);
if (paramWrapper.isExported && !paramWrapper.isResolved) {
if (!paramWrapper.isResolved) {
isResolved = false;
}
return paramWrapper.instance;
@@ -142,7 +142,7 @@ export class Injector {
const components = module.components;
const instanceWrapper = await this.scanForComponent(components, name, module, wrapper, context);
if (!instanceWrapper.isResolved && !instanceWrapper.isExported) {
if (!instanceWrapper.isResolved) {
await this.loadInstanceOfComponent(instanceWrapper, module);
}
if (instanceWrapper.async) {
@@ -162,18 +162,10 @@ export class Injector {
public async scanForComponentInExports(components: Map<string, any>, name: any, module: Module, metatype, context: Module[] = []) {
const instanceWrapper = await this.scanForComponentInRelatedModules(module, name, context);
if (!isNil(instanceWrapper)) {
return instanceWrapper;
}
const { exports } = module;
if (!exports.has(metatype.name)) {
if (isNil(instanceWrapper)) {
throw new UnknownDependenciesException(metatype.name);
}
return {
instance: null,
isResolved: false,
isExported: true,
};
return instanceWrapper;
}
public async scanForComponentInScopes(context: Module[], name: any, metatype) {

View File

@@ -19,8 +19,10 @@ export class ModuleTokenFactory {
public getScopeStack(scope: NestModuleMetatype[]): string[] {
const reversedScope = scope.reverse();
const firstGlobalIndex = reversedScope.findIndex((s) => this.reflectScope(s) === 'global');
const scopeStack = scope.reverse().slice(scope.length - firstGlobalIndex - 1);
return scopeStack.map((module) => module.name);
scope.reverse();
const stack = firstGlobalIndex >= 0 ?
scope.slice(scope.length - firstGlobalIndex - 1) : scope;
return stack.map((module) => module.name);
}
private reflectScope(metatype: NestModuleMetatype) {

View File

@@ -88,6 +88,10 @@ export class Module {
}
public addInjectable(injectable: Metatype<Injectable>) {
if (this.isCustomProvider(injectable)) {
this.addCustomProvider(injectable, this._injectables);
return;
}
this._injectables.set(injectable.name, {
name: injectable.name,
metatype: injectable,
@@ -97,8 +101,8 @@ export class Module {
}
public addComponent(component: ComponentMetatype) {
if (this.isCustomComponent(component)) {
this.addCustomComponent(component);
if (this.isCustomProvider(component)) {
this.addCustomProvider(component, this._components);
return;
}
this._components.set((component as Metatype<Injectable>).name, {
@@ -109,11 +113,11 @@ export class Module {
});
}
public isCustomComponent(component: ComponentMetatype): component is CustomClass | CustomFactory | CustomValue {
public isCustomProvider(component: ComponentMetatype): component is CustomClass | CustomFactory | CustomValue {
return !isNil((component as CustomComponent).provide);
}
public addCustomComponent(component: CustomFactory | CustomValue | CustomClass) {
public addCustomProvider(component: CustomFactory | CustomValue | CustomClass, collection: Map<string, any>) {
const { provide } = component;
const name = isFunction(provide) ? provide.name : provide;
const comp = {
@@ -121,9 +125,9 @@ export class Module {
name,
};
if (this.isCustomClass(comp)) this.addCustomClass(comp);
else if (this.isCustomValue(comp)) this.addCustomValue(comp);
else if (this.isCustomFactory(comp)) this.addCustomFactory(comp);
if (this.isCustomClass(comp)) this.addCustomClass(comp, collection);
else if (this.isCustomValue(comp)) this.addCustomValue(comp, collection);
else if (this.isCustomFactory(comp)) this.addCustomFactory(comp, collection);
}
public isCustomClass(component): component is CustomClass {
@@ -138,9 +142,9 @@ export class Module {
return !isUndefined((component as CustomFactory).useFactory);
}
public addCustomClass(component: CustomClass) {
public addCustomClass(component: CustomClass, collection: Map<string, any>) {
const { provide, name, useClass } = component;
this._components.set(name, {
collection.set(name, {
name,
metatype: useClass,
instance: null,
@@ -148,9 +152,9 @@ export class Module {
});
}
public addCustomValue(component: CustomValue) {
public addCustomValue(component: CustomValue, collection: Map<string, any>) {
const { provide, name, useValue: value } = component;
this._components.set(name, {
collection.set(name, {
name,
metatype: null,
instance: value,
@@ -160,9 +164,9 @@ export class Module {
});
}
public addCustomFactory(component: CustomFactory) {
public addCustomFactory(component: CustomFactory, collection: Map<string, any>) {
const { provide, name, useFactory: factory, inject } = component;
this._components.set(name, {
collection.set(name, {
name,
metatype: factory as any,
instance: null,
@@ -173,7 +177,7 @@ export class Module {
}
public addExportedComponent(exportedComponent: ComponentMetatype) {
if (this.isCustomComponent(exportedComponent)) {
if (this.isCustomProvider(exportedComponent)) {
this.addCustomExportedComponent(exportedComponent);
return;
}
@@ -201,7 +205,10 @@ export class Module {
}
public replace(toReplace, options) {
this.addComponent({
if (options.isComponent) {
return this.addComponent({ provide: toReplace, ...options });
}
this.addInjectable({
provide: toReplace,
...options,
});

View File

@@ -7,6 +7,7 @@ import { Metatype, MiddlewaresConsumer } from '@nestjs/common/interfaces';
import { MiddlewareConfigProxy } from '@nestjs/common/interfaces/middlewares';
import { RoutesMapper } from './routes-mapper';
import { NestMiddleware } from '@nestjs/common';
import { filterMiddlewares } from './utils';
export class MiddlewareBuilder implements MiddlewaresConsumer {
private readonly middlewaresCollection = new Set<MiddlewareConfiguration>();
@@ -54,10 +55,10 @@ export class MiddlewareBuilder implements MiddlewaresConsumer {
private readonly builder: MiddlewareBuilder,
middlewares,
) {
this.includedRoutes = this.filterMiddlewares(middlewares);
this.includedRoutes = filterMiddlewares(middlewares);
}
public with(...args): this {
public with(...args): MiddlewareConfigProxy {
this.contextArgs = args;
return this;
}
@@ -81,28 +82,5 @@ export class MiddlewareBuilder implements MiddlewaresConsumer {
private mapRoutesToFlatList(forRoutes) {
return forRoutes.reduce((a, b) => a.concat(b));
}
private filterMiddlewares(middlewares) {
return [].concat(middlewares)
.filter(isFunction)
.map((middleware) => {
if (this.isClass(middleware)) {
return middleware;
}
return AssignToken(class {
public resolve = (...args) => (req, res, next) => middleware(req, res, next);
});
});
}
private isClass(middleware) {
return middleware.toString().substring(0, 5) === 'class';
}
};
}
const AssignToken = (metatype): Metatype<any> => {
this.id = this.id || 1;
Object.defineProperty(metatype, 'name', { value: ++this.id });
return metatype;
};

View File

@@ -0,0 +1,27 @@
import { isFunction } from '@nestjs/common/utils/shared.utils';
import { Metatype } from '@nestjs/common/interfaces';
export const filterMiddlewares = (middlewares) => {
return [].concat(middlewares)
.filter(isFunction)
.map((middleware) => mapToClass(middleware));
};
export const mapToClass = (middleware) => {
if (this.isClass(middleware)) {
return middleware;
}
return assignToken(class {
public resolve = (...args) => (req, res, next) => middleware(req, res, next);
});
};
export const isClass = (middleware) => {
return middleware.toString().substring(0, 5) === 'class';
};
export const assignToken = (metatype): Metatype<any> => {
this.id = this.id || 1;
Object.defineProperty(metatype, 'name', { value: ++this.id });
return metatype;
};

View File

@@ -43,12 +43,17 @@ export class NestFactory {
}
private static async initialize(module) {
try {
this.logger.log(messages.APPLICATION_START);
await ExceptionsZone.asyncRun(async () => {
this.dependenciesScanner.scan(module);
await this.instanceLoader.createInstancesOfDependencies();
});
}
catch (e) {
process.abort();
}
}
private static createProxy(target) {
const proxy = this.createExceptionProxy();

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/core",
"version": "2.0.5",
"version": "4.0.0",
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -12,10 +12,10 @@
"iterare": "0.0.8"
},
"peerDependencies": {
"@nestjs/common": "~2.*",
"@nestjs/websockets": "~2.*",
"@nestjs/microservices": "~2.*",
"@nestjs/common": "~4.*",
"@nestjs/websockets": "~4.*",
"@nestjs/microservices": "~4.*",
"reflect-metadata": "0.1.10",
"rxjs": "^5.0.3"
"rxjs": "^5.4.2"
}
}

View File

@@ -18,4 +18,28 @@ describe('ApplicationConfig', () => {
expect(appConfig.getGlobalPrefix()).to.be.eql('');
});
});
describe('IOAdapter', () => {
it('should set io adapter', () => {
const ioAdapter = { test: 0 };
appConfig.setIoAdapter(ioAdapter as any);
expect(appConfig.getIoAdapter()).to.be.eql(ioAdapter);
});
});
describe('Pipes', () => {
it('should set global pipes', () => {
const pipes = ['test', 'test2'];
appConfig.useGlobalPipes(...pipes as any);
expect(appConfig.getGlobalPipes()).to.be.eql(pipes);
});
});
describe('Filters', () => {
it('should set global filters', () => {
const filters = ['test', 'test2'];
appConfig.useGlobalFilters(...filters as any);
expect(appConfig.getGlobalFilters()).to.be.eql(filters);
});
});
});

View File

@@ -32,7 +32,7 @@ describe('ExceptionsHandler', () => {
handler.next(new Error(), response);
expect(statusStub.calledWith(500)).to.be.true;
expect(jsonStub.calledWith({ message: 'Unknown exception' })).to.be.true;
expect(jsonStub.calledWith({ statusCode: 500, message: 'Unknown exception' })).to.be.true;
});
describe('when exception is instance of HttpException', () => {
it('should method send expected response status code and json object', () => {
@@ -52,7 +52,7 @@ describe('ExceptionsHandler', () => {
handler.next(new HttpException(message, status), response);
expect(statusStub.calledWith(status)).to.be.true;
expect(jsonStub.calledWith({ message })).to.be.true;
expect(jsonStub.calledWith({ message, statusCode: status })).to.be.true;
});
});
describe('when "invokeCustomFilters" returns true', () => {

View File

@@ -0,0 +1,51 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { GuardsConsumer } from './../../guards/guards-consumer';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
describe('GuardsConsumer', () => {
let consumer: GuardsConsumer;
let guards: any[];
beforeEach(() => {
consumer = new GuardsConsumer();
guards = [{ canActivate: () => true }, { canActivate: () => true }];
});
describe('tryActivate', () => {
describe('when guards array is empty', () => {
it('should return true', async () => {
const canActivate = await consumer.tryActivate([], {}, null, null);
expect(canActivate).to.be.true;
});
});
describe('when guards array is not empty', () => {
describe('when at least on guard returns false', () => {
it('should return false', async () => {
const canActivate = await consumer.tryActivate([
...guards,
{ canActivate: () => false },
], {}, null, null);
expect(canActivate).to.be.false;
});
});
describe('when each guard returns true', () => {
it('should return true', async () => {
const canActivate = await consumer.tryActivate(guards, {}, null, null);
expect(canActivate).to.be.true;
});
});
});
});
describe('pickResult', () => {
describe('when result is Observable', () => {
it('should returns promise', () => {
expect(consumer.pickResult(Observable.of(true))).to.eventually.instanceOf(Promise);
});
});
describe('when result is Promise', () => {
it('should await promise', async () => {
expect(await consumer.pickResult(Promise.resolve(true))).to.be.true;
});
});
});
});

View File

@@ -0,0 +1,61 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { GuardsContextCreator } from '../../guards/guards-context-creator';
import { Observable } from 'rxjs/Observable';
describe('GuardsContextCreator', () => {
let guardsContextCreator: GuardsContextCreator;
let guards: any[];
let container;
let getSpy;
beforeEach(() => {
guards = [
{
name: 'test',
instance: {
canActivate: () => true,
},
},
{
name: 'test2',
instance: {
canActivate: () => true,
},
},
{},
undefined,
];
getSpy = sinon.stub().returns({
injectables: new Map([
['test', guards[0]],
['test2', guards[1]],
]),
});
container = {
getModules: () => ({
get: getSpy,
}),
};
guardsContextCreator = new GuardsContextCreator(container as any);
});
describe('createConcreteContext', () => {
describe('when `moduleContext` is nil', () => {
it('should returns empty array', () => {
const result = guardsContextCreator.createConcreteContext(guards);
expect(result).to.be.empty;
});
});
describe('when `moduleContext` is defined', () => {
beforeEach(() => {
// tslint:disable-next-line:no-string-literal
guardsContextCreator['moduleContext'] = 'test';
});
it('should filter metatypes', () => {
expect(
guardsContextCreator.createConcreteContext(guards),
).to.have.length(2);
});
});
});
});

View File

@@ -37,4 +37,16 @@ describe('NestContainer', () => {
expect(() => container.addExportedComponent(null, 'TestModule')).throw(UnknownModuleException);
});
it('should "addInjectable" throw "UnknownModuleException" when module is not stored in collection', () => {
expect(() => container.addInjectable(null, 'TestModule')).throw(UnknownModuleException);
});
describe('clear', () => {
it('should call `clear` on modules collection', () => {
const clearSpy = sinon.spy((container as any).modules, 'clear');
container.clear();
expect(clearSpy.called).to.be.true;
});
});
});

View File

@@ -6,6 +6,9 @@ import { Component } from '../../../common/utils/decorators/component.decorator'
import { RuntimeException } from '../../errors/exceptions/runtime.exception';
import { Module } from '../../injector/module';
import { UnknownDependenciesException } from '../../errors/exceptions/unknown-dependencies.exception';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
describe('Injector', () => {
let injector: Injector;
@@ -57,25 +60,25 @@ describe('Injector', () => {
moduleDeps.components.set('DependencyTwo', depTwo);
});
it('should create an instance of component with proper dependencies', () => {
injector.loadInstance(mainTest, moduleDeps.components, moduleDeps);
it('should create an instance of component with proper dependencies', async () => {
await injector.loadInstance(mainTest, moduleDeps.components, moduleDeps);
const { instance } = moduleDeps.components.get('MainTest') as InstanceWrapper<MainTest>;
expect(instance.depOne instanceof DependencyOne).to.be.true;
expect(instance.depTwo instanceof DependencyOne).to.be.true;
expect(instance instanceof MainTest).to.be.true;
expect(instance.depOne).instanceof(DependencyOne);
expect(instance.depTwo).instanceof(DependencyOne);
expect(instance).instanceof(MainTest);
});
it('should set "isResolved" property to true after instance initialization', () => {
injector.loadInstance(mainTest, moduleDeps.components, moduleDeps);
it('should set "isResolved" property to true after instance initialization', async () => {
await injector.loadInstance(mainTest, moduleDeps.components, moduleDeps);
const { isResolved } = moduleDeps.components.get('MainTest') as InstanceWrapper<MainTest>;
expect(isResolved).to.be.true;
});
it('should throw RuntimeException when type is not stored in collection', () => {
expect(
injector.loadInstance.bind(injector, 'Test', moduleDeps.components, moduleDeps),
).to.throw(RuntimeException);
return expect(
injector.loadInstance({} as any, moduleDeps.components, moduleDeps),
).to.eventually.be.rejected;
});
});
@@ -112,8 +115,10 @@ describe('Injector', () => {
});
describe('resolveSingleParam', () => {
it('should throw "RuntimeException" when param is undefined', () => {
expect(() => injector.resolveSingleParam(null, undefined, null, [])).throws(RuntimeException);
it('should throw "RuntimeException" when param is undefined', async () => {
return expect(
injector.resolveSingleParam(null, undefined, null, []),
).to.eventually.be.rejected;
});
});
@@ -159,22 +164,22 @@ describe('Injector', () => {
(injector as any).scanForComponentInRelatedModules = scanForComponentInRelatedModules;
});
it('should return object from collection if exists', () => {
it('should return object from collection if exists', async () => {
const instance = { test: 3 };
const collection = {
has: () => true,
get: () => instance,
};
const result = injector.scanForComponent(collection as any, metatype.name, null, metatype);
const result = await injector.scanForComponent(collection as any, metatype.name, null, metatype);
expect(result).to.be.equal(instance);
});
it('should call "scanForComponentInRelatedModules" when object is not in collection', () => {
it('should call "scanForComponentInRelatedModules" when object is not in collection', async () => {
scanForComponentInRelatedModules.returns({});
const collection = {
has: () => false,
};
injector.scanForComponent(collection as any, metatype.name, null, metatype);
await injector.scanForComponent(collection as any, metatype.name, null, metatype);
expect(scanForComponentInRelatedModules.called).to.be.true;
});
@@ -185,8 +190,8 @@ describe('Injector', () => {
};
const module = { exports: collection };
expect(
() => injector.scanForComponent(collection as any, metatype.name, module as any, { metatype }),
).throws(UnknownDependenciesException);
injector.scanForComponent(collection as any, metatype.name, module as any, { metatype }),
).to.eventually.be.rejected;
});
it('should not throw "UnknownDependenciesException" instanceWrapper is not null', () => {
@@ -196,8 +201,8 @@ describe('Injector', () => {
};
const module = { exports: collection };
expect(
() => injector.scanForComponent(collection as any, metatype.name, module as any, metatype),
).not.throws(UnknownDependenciesException);
injector.scanForComponent(collection as any, metatype.name, module as any, metatype),
).to.eventually.be.not.rejected;
});
});
@@ -206,7 +211,7 @@ describe('Injector', () => {
let loadInstanceOfComponent: sinon.SinonSpy;
const metatype = { name: 'test' };
const module = {
relatedModules: [],
relatedModules: new Map(),
};
beforeEach(() => {
@@ -214,40 +219,44 @@ describe('Injector', () => {
(injector as any).loadInstanceOfComponent = loadInstanceOfComponent;
});
it('should return null when there is no related modules', () => {
const result = injector.scanForComponentInRelatedModules(module as any, null, []);
it('should return null when there is no related modules', async () => {
const result = await injector.scanForComponentInRelatedModules(module as any, null, []);
expect(result).to.be.eq(null);
});
it('should return null when related modules do not have appropriate component', () => {
let module = {
relatedModules: [{
relatedModules: new Map([['key', {
components: {
has: () => false,
},
exports: {
has: () => true,
},
}],
}]] as any),
};
expect(injector.scanForComponentInRelatedModules(module as any, metatype as any, [])).to.be.eq(null);
expect(
injector.scanForComponentInRelatedModules(module as any, metatype as any, []),
).to.be.eventually.eq(null);
module = {
relatedModules: [{
relatedModules: new Map([['key', {
components: {
has: () => true,
},
exports: {
has: () => false,
},
}],
}]] as any),
};
expect(injector.scanForComponentInRelatedModules(module as any, metatype as any, [])).to.be.eq(null);
expect(
injector.scanForComponentInRelatedModules(module as any, metatype as any, []),
).to.eventually.be.eq(null);
});
it('should call "loadInstanceOfComponent" when component is not resolved', () => {
it('should call "loadInstanceOfComponent" when component is not resolved', async () => {
let module = {
relatedModules: [{
relatedModules: new Map([['key', {
components: {
has: () => true,
get: () => ({
@@ -257,15 +266,15 @@ describe('Injector', () => {
exports: {
has: () => true,
},
}],
}]] as any),
};
injector.scanForComponentInRelatedModules(module as any, metatype as any, []);
await injector.scanForComponentInRelatedModules(module as any, metatype as any, []);
expect(loadInstanceOfComponent.called).to.be.true;
});
it('should not call "loadInstanceOfComponent" when component is resolved', () => {
it('should not call "loadInstanceOfComponent" when component is resolved', async () => {
let module = {
relatedModules: [{
relatedModules: new Map([['key', {
components: {
has: () => true,
get: () => ({
@@ -275,9 +284,9 @@ describe('Injector', () => {
exports: {
has: () => true,
},
}],
}]] as any),
};
injector.scanForComponentInRelatedModules(module as any, metatype as any, []);
await injector.scanForComponentInRelatedModules(module as any, metatype as any, []);
expect(loadInstanceOfComponent.called).to.be.false;
});
@@ -285,19 +294,19 @@ describe('Injector', () => {
describe('scanForComponentInScopes', () => {
it('should returns null when component is not available in any scope', () => {
expect(injector.scanForComponentInScopes([], '', {})).to.be.null;
expect(injector.scanForComponentInScopes([], '', {})).to.eventually.be.null;
});
it('should returns wrapper when component is available in any scope', () => {
const component = 'test';
sinon.stub(injector, 'scanForComponentInScope').returns(component);
expect(injector.scanForComponentInScopes([{}] as any, '', {})).to.be.eql(component);
expect(injector.scanForComponentInScopes([{}] as any, '', {})).to.eventually.be.eql(component);
});
});
describe('scanForComponentInScope', () => {
it('should returns null when scope throws exception', () => {
sinon.stub(injector, 'scanForComponent').throws('exception');
expect(injector.scanForComponentInScope({} as any, '', {})).to.be.null;
expect(injector.scanForComponentInScope({} as any, '', {})).to.eventually.be.null;
});
});
});

View File

@@ -27,13 +27,14 @@ describe('InstanceLoader', () => {
mockContainer = sinon.mock(container);
});
it('should call "loadPrototypeOfInstance" for each component and route in each module', () => {
it('should call "loadPrototypeOfInstance" for each component and route in each module', async () => {
const injector = new Injector();
(loader as any).injector = injector;
const module = {
components: new Map(),
routes: new Map(),
injectables: new Map(),
metatype: { name: 'test' },
};
const componentWrapper = { instance: null, metatype: TestComponent };
@@ -51,18 +52,19 @@ describe('InstanceLoader', () => {
sinon.stub(injector, 'loadInstanceOfRoute');
sinon.stub(injector, 'loadInstanceOfComponent');
loader.createInstancesOfDependencies();
await loader.createInstancesOfDependencies();
expect(loadComponentPrototypeStub.calledWith(componentWrapper, module.components)).to.be.true;
expect(loadComponentPrototypeStub.calledWith(routeWrapper, module.components)).to.be.true;
});
it('should call "loadInstanceOfComponent" for each component in each module', () => {
it('should call "loadInstanceOfComponent" for each component in each module', async () => {
const injector = new Injector();
(loader as any).injector = injector;
const module = {
components: new Map(),
routes: new Map(),
injectables: new Map(),
metatype: { name: 'test' },
};
const testComp = { instance: null, metatype: TestComponent, name: 'TestComponent' };
@@ -76,17 +78,18 @@ describe('InstanceLoader', () => {
const loadComponentStub = sinon.stub(injector, 'loadInstanceOfComponent');
sinon.stub(injector, 'loadInstanceOfRoute');
loader.createInstancesOfDependencies();
await loader.createInstancesOfDependencies();
expect(loadComponentStub.calledWith(module.components.get('TestComponent'), module)).to.be.true;
});
it('should call "loadInstanceOfRoute" for each route in each module', () => {
it('should call "loadInstanceOfRoute" for each route in each module', async () => {
const injector = new Injector();
(loader as any).injector = injector;
const module = {
components: new Map(),
routes: new Map(),
injectables: new Map(),
metatype: { name: 'test' },
};
const wrapper = { name: 'TestRoute', instance: null, metatype: TestRoute };
@@ -99,7 +102,7 @@ describe('InstanceLoader', () => {
sinon.stub(injector, 'loadInstanceOfComponent');
const loadRoutesStub = sinon.stub(injector, 'loadInstanceOfRoute');
loader.createInstancesOfDependencies();
await loader.createInstancesOfDependencies();
expect(loadRoutesStub.calledWith(module.routes.get('TestRoute'), module)).to.be.true;
});

View File

@@ -37,6 +37,29 @@ describe('Module', () => {
}]);
});
it('should add injectable', () => {
const collection = new Map();
const setSpy = sinon.spy(collection, 'set');
(module as any)._injectables = collection;
module.addInjectable(TestComponent);
expect(setSpy.getCall(0).args).to.deep.equal([ 'TestComponent', {
name: 'TestComponent',
metatype: TestComponent,
instance: null,
isResolved: false,
}]);
});
describe('when injectable is custom provided', () => {
it('should call `addCustomProvider`', () => {
const addCustomProviderSpy = sinon.spy(module, 'addCustomProvider');
module.addInjectable({ provide: 'test' } as any);
expect(addCustomProviderSpy.called).to.be.true;
});
});
it('should add component', () => {
const collection = new Map();
const setSpy = sinon.spy(collection, 'set');
@@ -51,14 +74,14 @@ describe('Module', () => {
}]);
});
it('should call "addCustomComponent" when "provide" property exists', () => {
const addCustomComponent = sinon.spy();
module.addCustomComponent = addCustomComponent;
it('should call "addCustomProvider" when "provide" property exists', () => {
const addCustomProvider = sinon.spy();
module.addCustomProvider = addCustomProvider;
const provider = { provide: 'test', useValue: 'test' };
module.addComponent(provider as any);
expect((addCustomComponent as sinon.SinonSpy).called).to.be.true;
expect((addCustomProvider as sinon.SinonSpy).called).to.be.true;
});
it('should call "addCustomClass" when "useClass" property exists', () => {
@@ -67,7 +90,7 @@ describe('Module', () => {
const provider = { provide: 'test', useClass: () => null };
module.addCustomComponent(provider as any);
module.addCustomProvider(provider as any, new Map());
expect((addCustomClass as sinon.SinonSpy).called).to.be.true;
});
@@ -77,7 +100,7 @@ describe('Module', () => {
const provider = { provide: 'test', useValue: () => null };
module.addCustomComponent(provider as any);
module.addCustomProvider(provider as any, new Map());
expect((addCustomValue as sinon.SinonSpy).called).to.be.true;
});
@@ -87,7 +110,7 @@ describe('Module', () => {
const provider = { provide: 'test', useFactory: () => null };
module.addCustomComponent(provider as any);
module.addCustomProvider(provider as any, new Map());
expect((addCustomFactory as sinon.SinonSpy).called).to.be.true;
});
@@ -101,7 +124,7 @@ describe('Module', () => {
(module as any)._components = collection;
});
it('should store component', () => {
module.addCustomClass(component as any);
module.addCustomClass(component as any, (module as any)._components);
expect(setSpy.calledWith(component.name, {
name: component.name,
metatype: type,
@@ -124,13 +147,14 @@ describe('Module', () => {
});
it('should store component', () => {
module.addCustomValue(component as any);
module.addCustomValue(component as any, (module as any)._components);
expect(setSpy.calledWith(name, {
name,
metatype: null,
instance: value,
isResolved: true,
isNotMetatype: true,
async: false,
})).to.be.true;
});
});
@@ -147,7 +171,7 @@ describe('Module', () => {
(module as any)._components = collection;
});
it('should store component', () => {
module.addCustomFactory(component as any);
module.addCustomFactory(component as any, (module as any)._components);
expect(setSpy.getCall(0).args).to.deep.equal([component.name, {
name: component.name,
metatype: type,
@@ -175,4 +199,30 @@ describe('Module', () => {
});
});
describe('when exported component is custom provided', () => {
it('should call `addCustomExportedComponent`', () => {
const addCustomExportedComponentSpy = sinon.spy(module, 'addCustomExportedComponent');
module.addExportedComponent({ provide: 'test' } as any);
expect(addCustomExportedComponentSpy.called).to.be.true;
});
});
describe('replace', () => {
describe('when component', () => {
it('should call `addComponent`', () => {
const addComponentSpy = sinon.spy(module, 'addComponent');
module.replace(null, { isComponent: true });
expect(addComponentSpy.called).to.be.true;
});
});
describe('when guard', () => {
it('should call `addInjectable`', () => {
const addInjectableSpy = sinon.spy(module, 'addInjectable');
module.replace(null, {});
expect(addInjectableSpy.called).to.be.true;
});
});
});
});

View File

@@ -61,15 +61,15 @@ describe('MiddlewaresModule', () => {
const route = { path: 'Test' };
const configuration = {
middlewares: [ TestMiddleware ],
forRoutes: [ TestRoute ]
forRoutes: [ TestRoute ],
};
const useSpy = sinon.spy();
const app = { use: useSpy };
expect(MiddlewaresModule.setupRouteMiddleware.bind(
MiddlewaresModule, route, configuration, 'Test' as any, app as any,
)).throws(RuntimeException);
expect(
MiddlewaresModule.setupRouteMiddleware(route as any, configuration, 'Test' as any, app as any),
).to.eventually.be.rejectedWith(RuntimeException);
});
it('should throw "InvalidMiddlewareException" exception when middlewares does not have "resolve" method', () => {
@@ -95,9 +95,9 @@ describe('MiddlewaresModule', () => {
instance,
} as any);
expect(MiddlewaresModule.setupRouteMiddleware.bind(
MiddlewaresModule, route, configuration, moduleKey, app as any,
)).throws(InvalidMiddlewareException);
expect(
MiddlewaresModule.setupRouteMiddleware(route as any, configuration, moduleKey, app as any),
).to.be.rejectedWith(InvalidMiddlewareException);
});
it('should store middlewares when middleware is stored in container', () => {

View File

@@ -0,0 +1,63 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { filterMiddlewares, mapToClass, isClass, assignToken } from '../../middlewares/utils';
describe('middleware utils', () => {
class Test {}
function fnMiddleware(req, res, next) {}
describe('filterMiddlewares', () => {
let middlewares: any[];
beforeEach(() => {
middlewares = [Test, fnMiddleware, undefined, null];
});
it('should returns filtered middlewares', () => {
expect(
filterMiddlewares(middlewares),
).to.have.length(2);
});
});
describe('mapToClass', () => {
describe('when middleware is a class', () => {
it('should returns identity', () => {
const type = mapToClass(Test);
expect(type).to.eql(Test);
});
});
describe('when middleware is a function', () => {
it('should returns metatype', () => {
const metatype = mapToClass(fnMiddleware);
expect(metatype).to.not.eql(fnMiddleware);
});
it('should define `resolve` method', () => {
const metatype = mapToClass(fnMiddleware);
expect(new metatype().resolve).to.exist;
});
it('should encapsulate function', () => {
const spy = sinon.spy();
const metatype = mapToClass(spy);
new metatype().resolve()();
expect(spy.called).to.be.true;
});
});
});
describe('isClass', () => {
describe('when middleware is a class', () => {
it('should returns true', () => {
expect(isClass(Test)).to.be.true;
});
});
describe('when middleware is a function', () => {
it('should returns false', () => {
expect(isClass(fnMiddleware)).to.be.false;
});
});
});
describe('assignToken', () => {
describe('should define `name` property on metatype', () => {
const anonymousType = class {};
assignToken(anonymousType);
expect(anonymousType.name).to.exist;
});
});
});

View File

@@ -41,16 +41,6 @@ describe('RouterExceptionFilters', () => {
});
});
});
/*describe('reflectExceptionFilters', () => {
const filters = [ ExceptionFilter ];
@ExceptionFilters(...filters as any)
class WithMetadata {}
it('should returns EXCEPTION_FILTERS_METADATA metadata', () => {
expect(
exceptionFilter.reflectExceptionFilters(new WithMetadata()),
).to.be.eql(filters);
});
});*/
describe('reflectCatchExceptions', () => {
it('should returns FILTER_CATCH_EXCEPTIONS metadata', () => {
expect(

View File

@@ -7,6 +7,9 @@ import { RouteParamsFactory } from '../../router/route-params-factory';
import { PipesContextCreator } from '../../pipes/pipes-context-creator';
import { PipesConsumer } from '../../pipes/pipes-consumer';
import { ApplicationConfig } from '../../application-config';
import { GuardsConsumer } from '../../guards/guards-consumer';
import { GuardsContextCreator } from '../../guards/guards-context-creator';
import { NestContainer } from '../../injector/container';
describe('RouterExecutionContext', () => {
let contextCreator: RouterExecutionContext;
@@ -29,13 +32,14 @@ describe('RouterExecutionContext', () => {
contextCreator = new RouterExecutionContext(
factory, new PipesContextCreator(new ApplicationConfig()), consumer,
new GuardsContextCreator(new NestContainer()), new GuardsConsumer(),
);
});
describe('create', () => {
describe('when callback metadata is undefined', () => {
it('should only bind instance as an context', () => {
const instance = {};
contextCreator.create(instance, callback as any);
contextCreator.create(instance, callback as any, '');
expect(bindSpy.calledWith(instance)).to.be.true;
});
});
@@ -58,7 +62,7 @@ describe('RouterExecutionContext', () => {
beforeEach(() => {
instance = { foo: 'bar' };
proxyContext = contextCreator.create(instance, callback as any);
proxyContext = contextCreator.create(instance, callback as any, '');
});
it('should be a function', () => {
expect(proxyContext).to.be.a('function');

View File

@@ -57,7 +57,7 @@ describe('RouterExplorer', () => {
{ path: 'test', requestMethod: RequestMethod.GET },
];
routerBuilder.applyPathsToRouterProxy(null, paths as any, null);
routerBuilder.applyPathsToRouterProxy(null, paths as any, null, '');
expect(bindStub.calledWith(null, paths[0], null)).to.be.true;
expect(bindStub.callCount).to.be.eql(paths.length);

View File

@@ -45,4 +45,35 @@ describe('RouterProxy', () => {
});
});
describe('createExceptionLayerProxy', () => {
it('should method return thunk', () => {
const proxy = routerProxy.createExceptionLayerProxy(() => {}, handler);
expect(typeof proxy === 'function').to.be.true;
});
it('should method encapsulate callback passed as argument', () => {
const expectation = handlerMock.expects('next').once();
const proxy = routerProxy.createExceptionLayerProxy((err, req, res, next) => {
throw new HttpException('test', 500);
}, handler);
proxy(null, null, null, null);
expectation.verify();
});
it('should method encapsulate async callback passed as argument', (done) => {
const expectation = handlerMock.expects('next').once();
const proxy = routerProxy.createExceptionLayerProxy(async (err, req, res, next) => {
throw new HttpException('test', 500);
}, handler);
proxy(null, null, null, null);
setTimeout(() => {
expectation.verify();
done();
}, 0);
});
});
});

View File

@@ -33,7 +33,7 @@ describe('RoutesResolver', () => {
});
describe('setupRouters', () => {
it('should method setup controllers to express application instance', () => {
it('should method setup controllers to router instance', () => {
const routes = new Map();
routes.set('TestRoute', {
instance: new TestRoute(),
@@ -41,10 +41,7 @@ describe('RoutesResolver', () => {
});
const use = sinon.spy();
const applicationMock = { use };
routesResolver.setupRouters(routes, '', applicationMock as any);
expect(use.calledOnce).to.be.true;
routesResolver.setupRouters(routes, '', { use } as any);
expect(use.calledWith('/global', router)).to.be.true;
});

View File

@@ -5,6 +5,7 @@ import { Module } from '../../common/utils/decorators/module.decorator';
import { NestModule } from '../../common/interfaces/modules/nest-module.interface';
import { Component } from '../../common/utils/decorators/component.decorator';
import { Controller } from '../../common/utils/decorators/controller.decorator';
import { MetadataScanner } from '../metadata-scanner';
describe('DependenciesScanner', () => {
@@ -35,7 +36,7 @@ describe('DependenciesScanner', () => {
});
beforeEach(() => {
scanner = new DependenciesScanner(container);
scanner = new DependenciesScanner(container, new MetadataScanner());
});
afterEach(() => {

View File

@@ -28,8 +28,8 @@ export class RpcContextCreator {
const exceptionHandler = this.exceptionFiltersContext.create(instance, callback);
const pipes = this.pipesCreator.create(instance, callback);
const guards = this.guardsContextCreator.create(instance, callback, module);
const metatype = this.getDataMetatype(instance, callback);
return this.rpcProxy.create(async (data) => {
const canActivate = await this.guardsConsumer.tryActivate(guards, data, instance, callback);
if (!canActivate) {

View File

@@ -16,10 +16,8 @@ export class RpcExceptionsHandler {
}
const status = 'error';
if (!(exception instanceof RpcException)) {
return Observable.throw({
status,
message: messages.UNKNOWN_EXCEPTION_MESSAGE,
});
const message = messages.UNKNOWN_EXCEPTION_MESSAGE;
return Observable.throw({ status, message });
}
const res = exception.getError();
const message = isObject(res) ? res : ({ status, message: res });

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/microservices",
"version": "2.0.5",
"version": "4.0.0",
"description": "Nest - modern, fast, powerful node.js web framework (@microservices)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -10,12 +10,12 @@
"dependencies": {
"json-socket": "^0.2.1",
"iterare": "0.0.8",
"redis": "^2.6.5"
"redis": "^2.7.1"
},
"peerDependencies": {
"@nestjs/common": "~2.*",
"@nestjs/core": "~2.*",
"@nestjs/common": "~4.*",
"@nestjs/core": "~4.*",
"reflect-metadata": "0.1.10",
"rxjs": "^5.0.3"
"rxjs": "^5.4.2"
}
}

View File

@@ -6,6 +6,7 @@ import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/finally';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/of';
export abstract class Server {
protected readonly messageHandlers: MessageHandlers = {};

View File

@@ -0,0 +1,42 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { ExceptionFilters } from '../../../common/utils/decorators/exception-filters.decorator';
import { Catch } from '../../../common/utils/decorators/catch.decorator';
import { ExceptionFiltersContext } from './../../context/exception-filters-context';
describe('ExceptionFiltersContext', () => {
let moduleName: string;
let exceptionFilter: ExceptionFiltersContext;
class CustomException {}
@Catch(CustomException)
class ExceptionFilter {
public catch(exc, res) {}
}
beforeEach(() => {
moduleName = 'Test';
exceptionFilter = new ExceptionFiltersContext();
});
describe('create', () => {
describe('when filters metadata is empty', () => {
class EmptyMetadata {}
beforeEach(() => {
sinon.stub(exceptionFilter, 'createContext').returns([]);
});
it('should returns plain ExceptionHandler object', () => {
const filter = exceptionFilter.create(new EmptyMetadata(), () => ({}) as any);
expect((filter as any).filters).to.be.empty;
});
});
describe('when filters metadata is not empty', () => {
@ExceptionFilters(new ExceptionFilter())
class WithMetadata {}
it('should returns ExceptionHandler object with exception filters', () => {
const filter = exceptionFilter.create(new WithMetadata(), () => ({}) as any);
expect((filter as any).filters).to.not.be.empty;
});
});
});
});

View File

@@ -0,0 +1,131 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { Guard, Pipe, UseGuards, Component, UsePipes } from './../../../common';
import { RpcProxy } from './../../context/rpc-proxy';
import { RpcContextCreator } from './../../context/rpc-context-creator';
import { RpcExceptionsHandler } from '../../exceptions/rpc-exceptions-handler';
import { ExceptionFiltersContext } from './../../context/exception-filters-context';
import { PipesContextCreator } from '../../../core/pipes/pipes-context-creator';
import { PipesConsumer } from '../../../core/pipes/pipes-consumer';
import { PARAMTYPES_METADATA } from '../../../common/constants';
import { GuardsContextCreator } from '../../../core/guards/guards-context-creator';
import { GuardsConsumer } from '../../../core/guards/guards-consumer';
import { NestContainer } from '../../../core/injector/container';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { RpcException } from '../../index';
@Guard()
class TestGuard { canActivate: () => true; }
@Pipe()
class TestPipe {
transform(val) {
return val;
}
}
describe('RpcContextCreator', () => {
let contextCreator: RpcContextCreator;
let rpcProxy: RpcProxy;
let exceptionFiltersContext: ExceptionFiltersContext;
let pipesCreator: PipesContextCreator;
let pipesConsumer: PipesConsumer;
let guardsContextCreator: GuardsContextCreator;
let guardsConsumer: GuardsConsumer;
let instance: Test;
let module: string;
@UseGuards(TestGuard)
@Component()
class Test {
@UsePipes(new TestPipe())
test(data: string) {
return Observable.of(false);
}
}
beforeEach(() => {
rpcProxy = new RpcProxy();
exceptionFiltersContext = new ExceptionFiltersContext();
pipesCreator = new PipesContextCreator();
pipesConsumer = new PipesConsumer();
guardsContextCreator = new GuardsContextCreator(new NestContainer());
guardsConsumer = new GuardsConsumer();
contextCreator = new RpcContextCreator(
rpcProxy, exceptionFiltersContext, pipesCreator as any,
pipesConsumer as any, guardsContextCreator as any, guardsConsumer as any,
);
instance = new Test();
module = 'test';
});
describe('create', () => {
it('should create exception handler', () => {
const handlerCreateSpy = sinon.spy(exceptionFiltersContext, 'create');
contextCreator.create(instance, instance.test, module);
expect(handlerCreateSpy.calledWith(instance, instance.test)).to.be.true;
});
it('should create pipes context', () => {
const pipesCreateSpy = sinon.spy(pipesCreator, 'create');
contextCreator.create(instance, instance.test, module);
expect(pipesCreateSpy.calledWith(instance, instance.test)).to.be.true;
});
it('should create guards context', () => {
const guardsCreateSpy = sinon.spy(guardsContextCreator, 'create');
contextCreator.create(instance, instance.test, module);
expect(guardsCreateSpy.calledWith(instance, instance.test, module)).to.be.true;
});
describe('when proxy called', () => {
it('should call guards consumer `tryActivate`', async () => {
const tryActivateSpy = sinon.spy(guardsConsumer, 'tryActivate');
const proxy = await contextCreator.create(instance, instance.test, module);
const data = 'test';
await proxy(data);
expect(tryActivateSpy.called).to.be.true;
});
describe('when can activate', () => {
it('should call pipes consumer `applyPipes`', async () => {
const applyPipesSpy = sinon.spy(pipesConsumer, 'applyPipes');
const proxy = await contextCreator.create(instance, instance.test, module);
const data = 'test';
await proxy(data);
expect(applyPipesSpy.called).to.be.true;
});
});
describe('when can not activate', () => {
it('should throws forbidden exception', async () => {
const tryActivateStub = sinon.stub(guardsConsumer, 'tryActivate').returns(false);
const proxy = await contextCreator.create(instance, instance.test, module);
const data = 'test';
expect(proxy(data)).to.eventually.rejectedWith(RpcException);
});
});
});
});
describe('reflectCallbackParamtypes', () => {
it('should returns paramtypes array', () => {
const paramtypes = contextCreator.reflectCallbackParamtypes(instance, instance.test);
expect(paramtypes).to.be.eql([String]);
});
});
describe('getDataMetatype', () => {
describe('when paramtypes are reflected', () => {
it('should returns data paramtype', () => {
const type = contextCreator.getDataMetatype(instance, instance.test);
expect(type).to.be.eql(String);
});
});
describe('when paramtypes are not reflected', () => {
it('should returns null', () => {
const type = contextCreator.getDataMetatype(instance, () => ({}));
expect(type).to.be.null;
});
});
});
});

View File

@@ -0,0 +1,37 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { RpcProxy } from './../../context/rpc-proxy';
import { RpcExceptionsHandler } from './../../exceptions/rpc-exceptions-handler';
import { RpcException } from '../../exceptions/rpc-exception';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
describe('RpcProxy', () => {
let routerProxy: RpcProxy;
let handlerMock: sinon.SinonMock;
let handler: RpcExceptionsHandler;
beforeEach(() => {
handler = new RpcExceptionsHandler();
handlerMock = sinon.mock(handler);
routerProxy = new RpcProxy();
});
describe('create', () => {
it('should method return thunk', async () => {
const proxy = await routerProxy.create(async (data) => Observable.of(true), handler);
expect(typeof proxy === 'function').to.be.true;
});
it('should method encapsulate callback passed as argument', async () => {
const expectation = handlerMock.expects('handle').once();
const proxy = routerProxy.create(async (data) => {
throw new RpcException('test');
}, handler);
await proxy(null);
expectation.verify();
});
});
});

View File

@@ -0,0 +1,14 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { RpcException } from '../../exceptions/rpc-exception';
describe('RpcException', () => {
let instance: RpcException;
const error = 'test';
beforeEach(() => {
instance = new RpcException(error);
});
it('should returns error message or object', () => {
expect(instance.getError()).to.be.eql(error);
});
});

View File

@@ -0,0 +1,115 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { RpcExceptionsHandler } from './../../exceptions/rpc-exceptions-handler';
import { RpcException } from './../../exceptions/rpc-exception';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/empty';
describe('RpcExceptionsHandler', () => {
let handler: RpcExceptionsHandler;
beforeEach(() => {
handler = new RpcExceptionsHandler();
});
describe('handle', () => {
it('should method returns expected stream with message when exception is unknown', (done) => {
const stream$ = handler.handle(new Error());
stream$.catch((err) => {
expect(err).to.be.eql({ status: 'error', message: 'Unknown exception' });
done();
return Observable.empty();
}).subscribe(() => ({}));
});
describe('when exception is instance of WsException', () => {
it('should method emit expected status and json object', (done) => {
const message = {
custom: 'Unauthorized',
};
const stream$ = handler.handle(new RpcException(message));
stream$.catch((err) => {
expect(err).to.be.eql(message);
done();
return Observable.empty();
}).subscribe(() => ({}));
});
it('should method emit expected status and transform message to json', (done) => {
const message = 'Unauthorized';
const stream$ = handler.handle(new RpcException(message));
stream$.catch((err) => {
expect(err).to.be.eql({ message, status: 'error' });
done();
return Observable.empty();
}).subscribe(() => ({}));
});
});
describe('when "invokeCustomFilters" returns observable', () => {
const observable$ = Observable.of(true);
beforeEach(() => {
sinon.stub(handler, 'invokeCustomFilters').returns(observable$);
});
it('should returns observable', () => {
const result = handler.handle(new RpcException(''));
expect(result).to.be.eql(observable$);
});
});
});
describe('setCustomFilters', () => {
const filters = [ 'test', 'test2' ];
it('should set custom filters', () => {
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', () => {
expect(
() => handler.setCustomFilters(null),
).to.throw;
});
});
describe('invokeCustomFilters', () => {
describe('when filters array is empty', () => {
it('should returns identity', () => {
expect(handler.invokeCustomFilters(null)).to.be.null;
});
});
describe('when filters array is not empty', () => {
let filters, funcSpy;
class TestException {}
beforeEach(() => {
funcSpy = sinon.spy();
});
describe('when filter exists in filters array', () => {
beforeEach(() => {
filters = [
{ exceptionMetatypes: [ TestException ], func: funcSpy },
];
(handler as any).filters = filters;
});
it('should call funcSpy', () => {
handler.invokeCustomFilters(new TestException());
expect(funcSpy.notCalled).to.be.false;
});
it('should call funcSpy with exception and response passed as an arguments', () => {
const exception = new TestException();
handler.invokeCustomFilters(exception);
expect(funcSpy.calledWith(exception)).to.be.true;
});
it('should returns stream', () => {
expect(handler.invokeCustomFilters(new TestException())).to.be.not.null;
});
});
describe('when filter does not exists in filters array', () => {
it('should not call funcSpy', () => {
handler.invokeCustomFilters(new TestException());
expect(funcSpy.notCalled).to.be.true;
});
it('should returns null', () => {
expect(handler.invokeCustomFilters(new TestException())).to.be.null;
});
});
});
});
});

View File

@@ -4,6 +4,7 @@ import { ListenersController } from '../listeners-controller';
import { ListenerMetadataExplorer } from '../listener-metadata-explorer';
import { MetadataScanner } from '../../core/metadata-scanner';
import { ClientsContainer } from '../container';
import { RpcContextCreator } from '../context/rpc-context-creator';
describe('ListenersController', () => {
let instance: ListenersController,
@@ -17,7 +18,7 @@ describe('ListenersController', () => {
explorer = sinon.mock(metadataExplorer);
});
beforeEach(() => {
instance = new ListenersController(new ClientsContainer());
instance = new ListenersController(new ClientsContainer(), sinon.createStubInstance(RpcContextCreator) as any);
(instance as any).metadataExplorer = metadataExplorer;
addSpy = sinon.spy();
server = {
@@ -31,11 +32,8 @@ describe('ListenersController', () => {
{ pattern: 'test2', targetCallback: '2' },
];
explorer.expects('explore').returns(handlers);
instance.bindPatternHandlers(null, server);
instance.bindPatternHandlers(null, server, '');
expect(addSpy.calledTwice).to.be.true;
expect(addSpy.calledWith(handlers[0].pattern, handlers[0].targetCallback)).to.be.true;
expect(addSpy.calledWith(handlers[1].pattern, handlers[1].targetCallback)).to.be.true;
});
});
});

View File

@@ -95,7 +95,7 @@ describe('ServerRedis', () => {
});
it(`should publish NO_PATTERN_MESSAGE if pattern not exists in messageHandlers object`, () => {
server.handleMessage(channel, {}, null);
expect(getPublisherSpy.calledWith({ err: NO_PATTERN_MESSAGE })).to.be.true;
expect(getPublisherSpy.calledWith({ status: 'error', error: NO_PATTERN_MESSAGE })).to.be.true;
});
it(`should call handler with expected arguments`, () => {
const handler = sinon.spy();

View File

@@ -54,7 +54,7 @@ describe('ServerTCP', () => {
});
it('should send NO_PATTERN_MESSAGE error if key is not exists in handlers object', () => {
server.handleMessage(socket, msg);
expect(socket.sendMessage.calledWith({ err: NO_PATTERN_MESSAGE })).to.be.true;
expect(socket.sendMessage.calledWith({ status: 'error', error: NO_PATTERN_MESSAGE })).to.be.true;
});
it('should call handler if exists in handlers object', () => {
const handler = sinon.spy();

View File

@@ -7,7 +7,7 @@ import { Client } from './../../utils/client.decorator';
describe('@Client', () => {
const pattern = { role: 'test' };
class TestComponent {
@Client(pattern)
@Client(pattern as any)
public static instance;
}
it(`should enhance property with metadata`, () => {

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/testing",
"version": "2.0.3",
"version": "4.0.0",
"description": "Nest - modern, fast, powerful node.js web framework (@testing)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -8,7 +8,8 @@
"compile": "tsc -p tsconfig.json"
},
"peerDependencies": {
"@nestjs/common": "~2.*",
"@nestjs/core": "~2.*"
"@nestjs/common": "~4.*",
"@nestjs/core": "~4.*",
"@nestjs/microservices": "~4.*"
}
}

View File

@@ -26,14 +26,12 @@ export class TestingModuleBuilder {
this.scanner.scan(this.module);
}
public overrideGuard(typeOrToken): OverrideBy {
return this.override(typeOrToken, false);
}
public overrideComponent(typeOrToken): OverrideBy {
const addOverload = (options) => {
this.overloadsMap.set(typeOrToken, {
...options,
});
return this;
};
return this.createOverrideByBuilder(addOverload);
return this.override(typeOrToken, true);
}
public async compile(): Promise<TestingModule> {
@@ -47,6 +45,17 @@ export class TestingModuleBuilder {
return new TestingModule(this.container, [], root);
}
private override(typeOrToken, isComponent: boolean): OverrideBy {
const addOverload = (options) => {
this.overloadsMap.set(typeOrToken, {
...options,
isComponent,
});
return this;
};
return this.createOverrideByBuilder(addOverload);
}
private createOverrideByBuilder(add: (provider) => TestingModuleBuilder): OverrideBy {
return {
useValue: (value) => add({ useValue: value }),

View File

@@ -6,6 +6,9 @@ import { isFunction } from '@nestjs/common/utils/shared.utils';
import { ModuleTokenFactory } from '@nestjs/core/injector/module-token-factory';
import { NestModuleMetatype } from '@nestjs/common/interfaces/modules/module-metatype.interface';
import { UnknownModuleException } from './errors/unknown-module.exception';
import { NestApplication, NestMicroservice } from '@nestjs/core';
import { INestApplication, INestMicroservice } from '@nestjs/common';
import { MicroserviceConfiguration } from '@nestjs/microservices';
export class TestingModule {
private readonly moduleTokenFactory = new ModuleTokenFactory();
@@ -15,6 +18,14 @@ export class TestingModule {
private readonly scope: NestModuleMetatype[],
private readonly contextModule) {}
public createNestApplication(express?): INestApplication {
return new NestApplication(this.container, express);
}
public createNestMicroservice(config: MicroserviceConfiguration): INestMicroservice {
return new NestMicroservice(this.container, config);
}
public select<T>(module: Metatype<T>): TestingModule {
const modules = this.container.getModules();
const moduleMetatype = this.contextModule.metatype;

View File

@@ -13,18 +13,15 @@ export class WsExceptionsHandler {
const status = 'error';
if (!(exception instanceof WsException)) {
client.emit('exception', {
status,
message: messages.UNKNOWN_EXCEPTION_MESSAGE,
});
return;
const message = messages.UNKNOWN_EXCEPTION_MESSAGE;
return client.emit('exception', { status, message });
}
const res = exception.getError();
const message = isObject(res) ? res : ({
const result = exception.getError();
const message = isObject(result) ? result : ({
status,
message: res,
message: result,
});
client.emit('exception', { status, message });
client.emit('exception', message);
}
public setCustomFilters(filters: ExceptionFilterMetadata[]) {

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/websockets",
"version": "2.0.3",
"version": "4.0.0",
"description": "Nest - modern, fast, powerful node.js web framework (@websockets)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -12,9 +12,9 @@
"socket.io": "^1.7.2"
},
"peerDependencies": {
"@nestjs/common": "~2.*",
"@nestjs/core": "~2.*",
"@nestjs/common": "~4.*",
"@nestjs/core": "~4.*",
"reflect-metadata": "0.1.10",
"rxjs": "^5.0.3"
"rxjs": "^5.4.2"
}
}

View File

@@ -0,0 +1,42 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { ExceptionFilters } from '../../../common/utils/decorators/exception-filters.decorator';
import { Catch } from '../../../common/utils/decorators/catch.decorator';
import { ExceptionFiltersContext } from './../../context/exception-filters-context';
describe('ExceptionFiltersContext', () => {
let moduleName: string;
let exceptionFilter: ExceptionFiltersContext;
class CustomException {}
@Catch(CustomException)
class ExceptionFilter {
public catch(exc, res) {}
}
beforeEach(() => {
moduleName = 'Test';
exceptionFilter = new ExceptionFiltersContext();
});
describe('create', () => {
describe('when filters metadata is empty', () => {
class EmptyMetadata {}
beforeEach(() => {
sinon.stub(exceptionFilter, 'createContext').returns([]);
});
it('should returns plain ExceptionHandler object', () => {
const filter = exceptionFilter.create(new EmptyMetadata(), () => ({}) as any);
expect((filter as any).filters).to.be.empty;
});
});
describe('when filters metadata is not empty', () => {
@ExceptionFilters(new ExceptionFilter())
class WithMetadata {}
it('should returns ExceptionHandler object with exception filters', () => {
const filter = exceptionFilter.create(new WithMetadata(), () => ({}) as any);
expect((filter as any).filters).to.not.be.empty;
});
});
});
});

View File

@@ -0,0 +1,131 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { Guard, Pipe, UseGuards, Component, UsePipes } from './../../../common';
import { WsProxy } from './../../context/ws-proxy';
import { WsContextCreator } from './../../context/ws-context-creator';
import { WsExceptionsHandler } from '../../exceptions/ws-exceptions-handler';
import { ExceptionFiltersContext } from './../../context/exception-filters-context';
import { PipesContextCreator } from '../../../core/pipes/pipes-context-creator';
import { PipesConsumer } from '../../../core/pipes/pipes-consumer';
import { PARAMTYPES_METADATA } from '../../../common/constants';
import { GuardsContextCreator } from '../../../core/guards/guards-context-creator';
import { GuardsConsumer } from '../../../core/guards/guards-consumer';
import { NestContainer } from '../../../core/injector/container';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { WsException } from '../../index';
@Guard()
class TestGuard { canActivate: () => true; }
@Pipe()
class TestPipe {
transform(val) {
return val;
}
}
describe('WsContextCreator', () => {
let contextCreator: WsContextCreator;
let wsProxy: WsProxy;
let exceptionFiltersContext: ExceptionFiltersContext;
let pipesCreator: PipesContextCreator;
let pipesConsumer: PipesConsumer;
let guardsContextCreator: GuardsContextCreator;
let guardsConsumer: GuardsConsumer;
let instance: Test;
let module: string;
@UseGuards(TestGuard)
@Component()
class Test {
@UsePipes(new TestPipe())
test(client: string, data: number) {
return Observable.of(false);
}
}
beforeEach(() => {
wsProxy = new WsProxy();
exceptionFiltersContext = new ExceptionFiltersContext();
pipesCreator = new PipesContextCreator();
pipesConsumer = new PipesConsumer();
guardsContextCreator = new GuardsContextCreator(new NestContainer());
guardsConsumer = new GuardsConsumer();
contextCreator = new WsContextCreator(
wsProxy, exceptionFiltersContext, pipesCreator as any,
pipesConsumer as any, guardsContextCreator as any, guardsConsumer as any,
);
instance = new Test();
module = 'test';
});
describe('create', () => {
it('should create exception handler', () => {
const handlerCreateSpy = sinon.spy(exceptionFiltersContext, 'create');
contextCreator.create(instance, instance.test, module);
expect(handlerCreateSpy.calledWith(instance, instance.test)).to.be.true;
});
it('should create pipes context', () => {
const pipesCreateSpy = sinon.spy(pipesCreator, 'create');
contextCreator.create(instance, instance.test, module);
expect(pipesCreateSpy.calledWith(instance, instance.test)).to.be.true;
});
it('should create guards context', () => {
const guardsCreateSpy = sinon.spy(guardsContextCreator, 'create');
contextCreator.create(instance, instance.test, module);
expect(guardsCreateSpy.calledWith(instance, instance.test, module)).to.be.true;
});
describe('when proxy called', () => {
it('should call guards consumer `tryActivate`', async () => {
const tryActivateSpy = sinon.spy(guardsConsumer, 'tryActivate');
const proxy = await contextCreator.create(instance, instance.test, module);
const data = 'test';
await proxy(null, data);
expect(tryActivateSpy.called).to.be.true;
});
describe('when can activate', () => {
it('should call pipes consumer `applyPipes`', async () => {
const applyPipesSpy = sinon.spy(pipesConsumer, 'applyPipes');
const proxy = await contextCreator.create(instance, instance.test, module);
const data = 'test';
await proxy(null, data);
expect(applyPipesSpy.called).to.be.true;
});
});
describe('when can not activate', () => {
it('should throws forbidden exception', async () => {
const tryActivateStub = sinon.stub(guardsConsumer, 'tryActivate').returns(false);
const proxy = await contextCreator.create(instance, instance.test, module);
const data = 'test';
expect(proxy(null, data)).to.eventually.rejectedWith(WsException);
});
});
});
});
describe('reflectCallbackParamtypes', () => {
it('should returns paramtypes array', () => {
const paramtypes = contextCreator.reflectCallbackParamtypes(instance, instance.test);
expect(paramtypes).to.be.eql([String, Number]);
});
});
describe('getDataMetatype', () => {
describe('when paramtypes are reflected', () => {
it('should returns data paramtype', () => {
const type = contextCreator.getDataMetatype(instance, instance.test);
expect(type).to.be.eql(Number);
});
});
describe('when paramtypes are not reflected', () => {
it('should returns null', () => {
const type = contextCreator.getDataMetatype(instance, () => ({}));
expect(type).to.be.null;
});
});
});
});

View File

@@ -0,0 +1,35 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { WsProxy} from './../../context/ws-proxy';
import { WsExceptionsHandler } from '../../exceptions/ws-exceptions-handler';
import { WsException } from '../../exceptions/ws-exception';
describe('WsProxy', () => {
let routerProxy: WsProxy;
let handlerMock: sinon.SinonMock;
let handler: WsExceptionsHandler;
beforeEach(() => {
handler = new WsExceptionsHandler();
handlerMock = sinon.mock(handler);
routerProxy = new WsProxy();
});
describe('create', () => {
it('should method return thunk', async () => {
const proxy = await routerProxy.create(async (client, data) => {}, handler);
expect(typeof proxy === 'function').to.be.true;
});
it('should method encapsulate callback passed as argument', async () => {
const expectation = handlerMock.expects('handle').once();
const proxy = routerProxy.create(async (client, data) => {
throw new WsException('test');
}, handler);
await proxy(null, null);
expectation.verify();
});
});
});

View File

@@ -0,0 +1,14 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { WsException } from '../../exceptions/ws-exception';
describe('WsException', () => {
let instance: WsException;
const error = 'test';
beforeEach(() => {
instance = new WsException(error);
});
it('should returns error message or object', () => {
expect(instance.getError()).to.be.eql(error);
});
});

View File

@@ -0,0 +1,111 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { WsExceptionsHandler } from './../../exceptions/ws-exceptions-handler';
import { WsException } from './../../exceptions/ws-exception';
describe('WsExceptionsHandler', () => {
let handler: WsExceptionsHandler;
let emitStub: sinon.SinonStub;
let client;
beforeEach(() => {
handler = new WsExceptionsHandler();
emitStub = sinon.stub();
client = {
emit: emitStub,
};
client.emit.returns(client);
});
describe('handle', () => {
it('should method emit expected status code message when exception is unknown', () => {
handler.handle(new Error(), client);
expect(emitStub.calledWith(
'exception',
{ status: 'error', message: 'Unknown exception' },
)).to.be.true;
});
describe('when exception is instance of WsException', () => {
it('should method emit expected status and json object', () => {
const message = {
custom: 'Unauthorized',
};
handler.handle(new WsException(message), client);
expect(emitStub.calledWith('exception', message)).to.be.true;
});
it('should method emit expected status and transform message to json', () => {
const message = 'Unauthorized';
handler.handle(new WsException(message), client);
expect(emitStub.calledWith('exception', { message, status: 'error' })).to.be.true;
});
});
describe('when "invokeCustomFilters" returns true', () => {
beforeEach(() => {
sinon.stub(handler, 'invokeCustomFilters').returns(true);
});
it('should not call `emit`', () => {
handler.handle(new WsException(''), client);
expect(emitStub.notCalled).to.be.true;
});
});
});
describe('setCustomFilters', () => {
const filters = [ 'test', 'test2' ];
it('should set custom filters', () => {
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', () => {
expect(
() => handler.setCustomFilters(null),
).to.throw;
});
});
describe('invokeCustomFilters', () => {
describe('when filters array is empty', () => {
it('should returns false', () => {
expect(handler.invokeCustomFilters(null, null)).to.be.false;
});
});
describe('when filters array is not empty', () => {
let filters, funcSpy;
class TestException {}
beforeEach(() => {
funcSpy = sinon.spy();
});
describe('when filter exists in filters array', () => {
beforeEach(() => {
filters = [
{ exceptionMetatypes: [ TestException ], func: funcSpy },
];
(handler as any).filters = filters;
});
it('should call funcSpy', () => {
handler.invokeCustomFilters(new TestException(), null);
expect(funcSpy.notCalled).to.be.false;
});
it('should call funcSpy with exception and response passed as an arguments', () => {
const exception = new TestException();
const res = { foo: 'bar' };
handler.invokeCustomFilters(exception, res);
expect(funcSpy.calledWith(exception, res)).to.be.true;
});
it('should returns true', () => {
expect(handler.invokeCustomFilters(new TestException(), null)).to.be.true;
});
});
describe('when filter does not exists in filters array', () => {
it('should not call funcSpy', () => {
handler.invokeCustomFilters(new TestException(), null);
expect(funcSpy.notCalled).to.be.true;
});
it('should returns false', () => {
expect(handler.invokeCustomFilters(new TestException(), null)).to.be.false;
});
});
});
});
});

View File

@@ -55,11 +55,11 @@ describe('GatewayMetadataExplorer', () => {
test = new Test();
});
it(`should return null when "isMessageMapping" metadata is undefined`, () => {
const metadata = instance.exploreMethodMetadata(test, Object.getPrototypeOf(test), 'noMessage');
const metadata = instance.exploreMethodMetadata(test, 'noMessage');
expect(metadata).to.eq(null);
});
it(`should return message mapping properties when "isMessageMapping" metadata is not undefined`, () => {
const metadata = instance.exploreMethodMetadata(test, Object.getPrototypeOf(test), 'test');
const metadata = instance.exploreMethodMetadata(test, 'test');
expect(metadata).to.have.keys([ 'callback', 'message' ]);
expect(metadata.message).to.eql(message);
});

View File

@@ -7,6 +7,7 @@ import { InvalidSocketPortException } from '../exceptions/invalid-socket-port.ex
import { GatewayMetadataExplorer } from '../gateway-metadata-explorer';
import { MetadataScanner } from '../../core/metadata-scanner';
import { ApplicationConfig } from '@nestjs/core/application-config';
import { WsContextCreator } from '../context/ws-context-creator';
describe('WebSocketsController', () => {
let instance: WebSocketsController;
@@ -24,7 +25,7 @@ describe('WebSocketsController', () => {
config = new ApplicationConfig();
provider = new SocketServerProvider(null, config);
mockProvider = sinon.mock(provider);
instance = new WebSocketsController(provider, null, config);
instance = new WebSocketsController(provider, null, config, sinon.createStubInstance(WsContextCreator));
});
describe('hookGatewayIntoServer', () => {
let subscribeObservableServer: sinon.SinonSpy;