mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
517 lines
18 KiB
TypeScript
517 lines
18 KiB
TypeScript
import { ForbiddenException } from '@nestjs/common/exceptions/forbidden.exception';
|
|
import { expect } from 'chai';
|
|
import { of } from 'rxjs';
|
|
import * as sinon from 'sinon';
|
|
import { PassThrough } from 'stream';
|
|
import { HttpException, HttpStatus, RouteParamMetadata } from '../../../common';
|
|
import { CUSTOM_ROUTE_ARGS_METADATA } from '../../../common/constants';
|
|
import { RouteParamtypes } from '../../../common/enums/route-paramtypes.enum';
|
|
import { AbstractHttpAdapter } from '../../adapters';
|
|
import { ApplicationConfig } from '../../application-config';
|
|
import { FORBIDDEN_MESSAGE } from '../../guards/constants';
|
|
import { GuardsConsumer } from '../../guards/guards-consumer';
|
|
import { GuardsContextCreator } from '../../guards/guards-context-creator';
|
|
import { HandlerResponseBasicFn } from '../../helpers/handler-metadata-storage';
|
|
import { NestContainer } from '../../injector/container';
|
|
import { InterceptorsConsumer } from '../../interceptors/interceptors-consumer';
|
|
import { InterceptorsContextCreator } from '../../interceptors/interceptors-context-creator';
|
|
import { PipesConsumer } from '../../pipes/pipes-consumer';
|
|
import { PipesContextCreator } from '../../pipes/pipes-context-creator';
|
|
import { RouteParamsFactory } from '../../router/route-params-factory';
|
|
import { RouterExecutionContext } from '../../router/router-execution-context';
|
|
import { HeaderStream } from '../../router/sse-stream';
|
|
import { NoopHttpAdapter } from '../utils/noop-adapter.spec';
|
|
|
|
describe('RouterExecutionContext', () => {
|
|
let contextCreator: RouterExecutionContext;
|
|
let callback: any;
|
|
let applySpy: sinon.SinonSpy;
|
|
let factory: RouteParamsFactory;
|
|
let consumer: PipesConsumer;
|
|
let guardsConsumer: GuardsConsumer;
|
|
let interceptorsConsumer: InterceptorsConsumer;
|
|
let adapter: AbstractHttpAdapter;
|
|
|
|
beforeEach(() => {
|
|
callback = {
|
|
bind: () => ({}),
|
|
apply: () => ({}),
|
|
};
|
|
applySpy = sinon.spy(callback, 'apply');
|
|
|
|
factory = new RouteParamsFactory();
|
|
consumer = new PipesConsumer();
|
|
guardsConsumer = new GuardsConsumer();
|
|
interceptorsConsumer = new InterceptorsConsumer();
|
|
adapter = new NoopHttpAdapter({});
|
|
contextCreator = new RouterExecutionContext(
|
|
factory,
|
|
new PipesContextCreator(new NestContainer(), new ApplicationConfig()),
|
|
consumer,
|
|
new GuardsContextCreator(new NestContainer()),
|
|
guardsConsumer,
|
|
new InterceptorsContextCreator(new NestContainer()),
|
|
interceptorsConsumer,
|
|
adapter,
|
|
);
|
|
});
|
|
describe('create', () => {
|
|
describe('when callback metadata is not undefined', () => {
|
|
let metadata: Record<number, RouteParamMetadata>;
|
|
let exchangeKeysForValuesSpy: sinon.SinonSpy;
|
|
beforeEach(() => {
|
|
metadata = {
|
|
[RouteParamtypes.NEXT]: { index: 0 },
|
|
[RouteParamtypes.BODY]: {
|
|
index: 2,
|
|
data: 'test',
|
|
},
|
|
};
|
|
sinon
|
|
.stub((contextCreator as any).contextUtils, 'reflectCallbackMetadata')
|
|
.returns(metadata);
|
|
sinon
|
|
.stub(
|
|
(contextCreator as any).contextUtils,
|
|
'reflectCallbackParamtypes',
|
|
)
|
|
.returns([]);
|
|
exchangeKeysForValuesSpy = sinon.spy(
|
|
contextCreator,
|
|
'exchangeKeysForValues',
|
|
);
|
|
});
|
|
it('should call "exchangeKeysForValues" with expected arguments', done => {
|
|
const keys = Object.keys(metadata);
|
|
|
|
contextCreator.create({ foo: 'bar' }, callback, '', '', 0);
|
|
expect(exchangeKeysForValuesSpy.called).to.be.true;
|
|
expect(exchangeKeysForValuesSpy.calledWith(keys, metadata)).to.be.true;
|
|
done();
|
|
});
|
|
describe('returns proxy function', () => {
|
|
let proxyContext;
|
|
let instance;
|
|
let tryActivateStub;
|
|
beforeEach(() => {
|
|
instance = { foo: 'bar' };
|
|
|
|
const canActivateFn = contextCreator.createGuardsFn(
|
|
[1] as any,
|
|
null,
|
|
null,
|
|
);
|
|
sinon.stub(contextCreator, 'createGuardsFn').returns(canActivateFn);
|
|
tryActivateStub = sinon
|
|
.stub(guardsConsumer, 'tryActivate')
|
|
.callsFake(async () => true);
|
|
proxyContext = contextCreator.create(instance, callback, '', '', 0);
|
|
});
|
|
it('should be a function', () => {
|
|
expect(proxyContext).to.be.a('function');
|
|
});
|
|
describe('when proxy function called', () => {
|
|
let request;
|
|
const response = {
|
|
status: () => response,
|
|
send: () => response,
|
|
json: () => response,
|
|
};
|
|
const next = {};
|
|
|
|
beforeEach(() => {
|
|
request = {
|
|
body: {
|
|
test: 3,
|
|
},
|
|
};
|
|
});
|
|
it('should apply expected context and arguments to callback', done => {
|
|
tryActivateStub.callsFake(async () => true);
|
|
proxyContext(request, response, next).then(() => {
|
|
const args = [next, undefined, request.body.test];
|
|
expect(applySpy.called).to.be.true;
|
|
expect(applySpy.calledWith(instance, args)).to.be.true;
|
|
done();
|
|
});
|
|
});
|
|
it('should throw exception when "tryActivate" returns false', async () => {
|
|
tryActivateStub.callsFake(async () => false);
|
|
|
|
let error: HttpException;
|
|
try {
|
|
await proxyContext(request, response, next);
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
expect(error).to.be.instanceOf(ForbiddenException);
|
|
expect(error.message).to.be.eql('Forbidden resource');
|
|
expect(error.getResponse()).to.be.eql({
|
|
statusCode: HttpStatus.FORBIDDEN,
|
|
error: 'Forbidden',
|
|
message: FORBIDDEN_MESSAGE,
|
|
});
|
|
});
|
|
it('should apply expected context when "canActivateFn" apply', () => {
|
|
proxyContext(request, response, next).then(() => {
|
|
expect(tryActivateStub.args[0][1][0]).to.equals(request);
|
|
expect(tryActivateStub.args[0][1][1]).to.equals(response);
|
|
expect(tryActivateStub.args[0][1][2]).to.equals(next);
|
|
});
|
|
});
|
|
it('should apply expected context when "intercept" apply', () => {
|
|
const interceptStub = sinon.stub(interceptorsConsumer, 'intercept');
|
|
proxyContext(request, response, next).then(() => {
|
|
expect(interceptStub.args[0][1][0]).to.equals(request);
|
|
expect(interceptStub.args[0][1][1]).to.equals(response);
|
|
expect(interceptStub.args[0][1][2]).to.equals(next);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('exchangeKeysForValues', () => {
|
|
it('should exchange arguments keys for appropriate values', () => {
|
|
const metadata = {
|
|
[RouteParamtypes.REQUEST]: { index: 0, data: 'test', pipes: [] },
|
|
[RouteParamtypes.BODY]: { index: 2, data: 'test', pipes: [] },
|
|
[`key${CUSTOM_ROUTE_ARGS_METADATA}`]: {
|
|
index: 3,
|
|
data: 'custom',
|
|
pipes: [],
|
|
},
|
|
};
|
|
const keys = Object.keys(metadata);
|
|
const values = contextCreator.exchangeKeysForValues(keys, metadata, '');
|
|
const expectedValues = [
|
|
{ index: 0, type: RouteParamtypes.REQUEST, data: 'test' },
|
|
{ index: 2, type: RouteParamtypes.BODY, data: 'test' },
|
|
{ index: 3, type: `key${CUSTOM_ROUTE_ARGS_METADATA}`, data: 'custom' },
|
|
];
|
|
expect(values[0]).to.deep.include(expectedValues[0]);
|
|
expect(values[1]).to.deep.include(expectedValues[1]);
|
|
});
|
|
});
|
|
|
|
describe('getParamValue', () => {
|
|
let consumerApplySpy: sinon.SinonSpy;
|
|
const value = 3,
|
|
metatype = null,
|
|
transforms = [{ transform: sinon.spy() }];
|
|
|
|
beforeEach(() => {
|
|
consumerApplySpy = sinon.spy(consumer, 'apply');
|
|
});
|
|
describe('when paramtype is query, body, rawBody or param', () => {
|
|
it('should call "consumer.apply" with expected arguments', () => {
|
|
contextCreator.getParamValue(
|
|
value,
|
|
{ metatype, type: RouteParamtypes.QUERY, data: null },
|
|
transforms,
|
|
);
|
|
expect(
|
|
consumerApplySpy.calledWith(
|
|
value,
|
|
{ metatype, type: RouteParamtypes.QUERY, data: null },
|
|
transforms,
|
|
),
|
|
).to.be.true;
|
|
|
|
contextCreator.getParamValue(
|
|
value,
|
|
{ metatype, type: RouteParamtypes.BODY, data: null },
|
|
transforms,
|
|
);
|
|
expect(
|
|
consumerApplySpy.calledWith(
|
|
value,
|
|
{ metatype, type: RouteParamtypes.BODY, data: null },
|
|
transforms,
|
|
),
|
|
).to.be.true;
|
|
|
|
contextCreator.getParamValue(
|
|
value,
|
|
{ metatype, type: RouteParamtypes.RAW_BODY, data: null },
|
|
transforms,
|
|
);
|
|
expect(
|
|
consumerApplySpy.calledWith(
|
|
value,
|
|
{ metatype, type: RouteParamtypes.RAW_BODY, data: null },
|
|
transforms,
|
|
),
|
|
).to.be.true;
|
|
|
|
contextCreator.getParamValue(
|
|
value,
|
|
{ metatype, type: RouteParamtypes.PARAM, data: null },
|
|
transforms,
|
|
);
|
|
expect(
|
|
consumerApplySpy.calledWith(
|
|
value,
|
|
{ metatype, type: RouteParamtypes.PARAM, data: null },
|
|
transforms,
|
|
),
|
|
).to.be.true;
|
|
});
|
|
});
|
|
});
|
|
describe('isPipeable', () => {
|
|
describe('when paramtype is not query, body, param and custom', () => {
|
|
it('should return false', () => {
|
|
const result = contextCreator.isPipeable(RouteParamtypes.NEXT);
|
|
expect(result).to.be.false;
|
|
});
|
|
it('otherwise', () => {
|
|
expect(contextCreator.isPipeable(RouteParamtypes.BODY)).to.be.true;
|
|
expect(contextCreator.isPipeable(RouteParamtypes.RAW_BODY)).to.be.true;
|
|
expect(contextCreator.isPipeable(RouteParamtypes.QUERY)).to.be.true;
|
|
expect(contextCreator.isPipeable(RouteParamtypes.PARAM)).to.be.true;
|
|
expect(contextCreator.isPipeable(RouteParamtypes.FILE)).to.be.true;
|
|
expect(contextCreator.isPipeable(RouteParamtypes.FILES)).to.be.true;
|
|
expect(contextCreator.isPipeable('custom')).to.be.true;
|
|
});
|
|
});
|
|
});
|
|
describe('createPipesFn', () => {
|
|
describe('when "paramsOptions" is empty', () => {
|
|
it('returns null', async () => {
|
|
const pipesFn = contextCreator.createPipesFn([], []);
|
|
expect(pipesFn).to.be.null;
|
|
});
|
|
});
|
|
});
|
|
describe('createGuardsFn', () => {
|
|
it('should throw ForbiddenException when "tryActivate" returns false', async () => {
|
|
const guardsFn = contextCreator.createGuardsFn([null], null, null);
|
|
sinon.stub(guardsConsumer, 'tryActivate').callsFake(async () => false);
|
|
|
|
let error: ForbiddenException;
|
|
try {
|
|
await guardsFn([]);
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
expect(error).to.be.instanceOf(ForbiddenException);
|
|
expect(error.message).to.be.eql('Forbidden resource');
|
|
expect(error.getResponse()).to.be.eql({
|
|
statusCode: HttpStatus.FORBIDDEN,
|
|
message: FORBIDDEN_MESSAGE,
|
|
error: 'Forbidden',
|
|
});
|
|
});
|
|
});
|
|
describe('createHandleResponseFn', () => {
|
|
describe('when "renderTemplate" is defined', () => {
|
|
beforeEach(() => {
|
|
sinon
|
|
.stub(adapter, 'render')
|
|
.callsFake((response, view: string, options: any) => {
|
|
return response.render(view, options);
|
|
});
|
|
});
|
|
it('should call "res.render()" with expected args', async () => {
|
|
const template = 'template';
|
|
const value = 'test';
|
|
const response = { render: sinon.spy() };
|
|
|
|
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(template);
|
|
|
|
const handler = contextCreator.createHandleResponseFn(
|
|
null,
|
|
true,
|
|
undefined,
|
|
200,
|
|
) as HandlerResponseBasicFn;
|
|
await handler(value, response);
|
|
|
|
expect(response.render.calledWith(template, value)).to.be.true;
|
|
});
|
|
});
|
|
describe('when "renderTemplate" is undefined', () => {
|
|
it('should not call "res.render()"', async () => {
|
|
const result = Promise.resolve('test');
|
|
const response = { render: sinon.spy() };
|
|
|
|
sinon.stub(contextCreator, 'reflectResponseHeaders').returns([]);
|
|
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(undefined);
|
|
sinon.stub(contextCreator, 'reflectSse').returns(undefined);
|
|
|
|
const handler = contextCreator.createHandleResponseFn(
|
|
null,
|
|
true,
|
|
undefined,
|
|
200,
|
|
) as HandlerResponseBasicFn;
|
|
await handler(result, response);
|
|
|
|
expect(response.render.called).to.be.false;
|
|
});
|
|
});
|
|
describe('when "redirectResponse" is present', () => {
|
|
beforeEach(() => {
|
|
sinon
|
|
.stub(adapter, 'redirect')
|
|
.callsFake((response, statusCode: number, url: string) => {
|
|
return response.redirect(statusCode, url);
|
|
});
|
|
});
|
|
it('should call "res.redirect()" with expected args', async () => {
|
|
const redirectResponse = {
|
|
url: 'http://test.com',
|
|
statusCode: 302,
|
|
};
|
|
const response = { redirect: sinon.spy() };
|
|
|
|
const handler = contextCreator.createHandleResponseFn(
|
|
() => {},
|
|
true,
|
|
redirectResponse,
|
|
200,
|
|
) as HandlerResponseBasicFn;
|
|
await handler(redirectResponse, response);
|
|
|
|
expect(
|
|
response.redirect.calledWith(
|
|
redirectResponse.statusCode,
|
|
redirectResponse.url,
|
|
),
|
|
).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe('when "redirectResponse" is undefined', () => {
|
|
it('should not call "res.redirect()"', async () => {
|
|
const result = Promise.resolve('test');
|
|
const response = { redirect: sinon.spy() };
|
|
|
|
sinon.stub(contextCreator, 'reflectResponseHeaders').returns([]);
|
|
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(undefined);
|
|
sinon.stub(contextCreator, 'reflectSse').returns(undefined);
|
|
|
|
const handler = contextCreator.createHandleResponseFn(
|
|
null,
|
|
true,
|
|
undefined,
|
|
200,
|
|
) as HandlerResponseBasicFn;
|
|
await handler(result, response);
|
|
|
|
expect(response.redirect.called).to.be.false;
|
|
});
|
|
});
|
|
|
|
describe('when replying with result', () => {
|
|
it('should call "adapter.reply()" with expected args', async () => {
|
|
const result = Promise.resolve('test');
|
|
const response = {};
|
|
|
|
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(undefined);
|
|
sinon.stub(contextCreator, 'reflectSse').returns(undefined);
|
|
|
|
const handler = contextCreator.createHandleResponseFn(
|
|
null,
|
|
false,
|
|
undefined,
|
|
1234,
|
|
) as HandlerResponseBasicFn;
|
|
const adapterReplySpy = sinon.spy(adapter, 'reply');
|
|
await handler(result, response);
|
|
expect(
|
|
adapterReplySpy.calledOnceWithExactly(
|
|
sinon.match.same(response),
|
|
'test',
|
|
1234,
|
|
),
|
|
).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe('when "isSse" is enabled', () => {
|
|
it('should delegate result to SseStream', async () => {
|
|
const result = of('test');
|
|
const response = new PassThrough();
|
|
response.write = sinon.spy();
|
|
|
|
const request = new PassThrough();
|
|
request.on = sinon.spy();
|
|
|
|
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(undefined);
|
|
sinon.stub(contextCreator, 'reflectSse').returns('/');
|
|
|
|
const handler = contextCreator.createHandleResponseFn(
|
|
null,
|
|
true,
|
|
undefined,
|
|
200,
|
|
) as HandlerResponseBasicFn;
|
|
await handler(result, response, request);
|
|
|
|
expect((response.write as any).called).to.be.true;
|
|
expect((request.on as any).called).to.be.true;
|
|
});
|
|
|
|
it('should not allow a non-observable result', async () => {
|
|
const result = Promise.resolve('test');
|
|
const response = new PassThrough();
|
|
const request = new PassThrough();
|
|
|
|
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(undefined);
|
|
sinon.stub(contextCreator, 'reflectSse').returns('/');
|
|
|
|
const handler = contextCreator.createHandleResponseFn(
|
|
null,
|
|
true,
|
|
undefined,
|
|
200,
|
|
) as HandlerResponseBasicFn;
|
|
|
|
try {
|
|
await handler(result, response, request);
|
|
} catch (e) {
|
|
expect(e.message).to.equal(
|
|
'You must return an Observable stream to use Server-Sent Events (SSE).',
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should apply any headers that exists on the response', async () => {
|
|
const result = of('test');
|
|
const response = new PassThrough() as HeaderStream;
|
|
response.write = sinon.spy();
|
|
response.writeHead = sinon.spy();
|
|
response.flushHeaders = sinon.spy();
|
|
response.getHeaders = sinon
|
|
.stub()
|
|
.returns({ 'access-control-headers': 'some-cors-value' });
|
|
|
|
const request = new PassThrough();
|
|
request.on = sinon.spy();
|
|
|
|
sinon.stub(contextCreator, 'reflectRenderTemplate').returns(undefined);
|
|
sinon.stub(contextCreator, 'reflectSse').returns('/');
|
|
|
|
const handler = contextCreator.createHandleResponseFn(
|
|
null,
|
|
true,
|
|
undefined,
|
|
200,
|
|
) as HandlerResponseBasicFn;
|
|
await handler(result, response, request);
|
|
|
|
expect(
|
|
(response.writeHead as sinon.SinonSpy).calledWith(
|
|
200,
|
|
sinon.match.hasNested('access-control-headers', 'some-cors-value'),
|
|
),
|
|
).to.be.true;
|
|
});
|
|
});
|
|
});
|
|
});
|