mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
Merge pull request #16246 from 0xC0FFE2/test/add-unit-tests-discovery-and-serializer
test: add unit tests for DiscoveryService and ClassSerializerInterceptor
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { ClassSerializerInterceptor } from '../../serializer/class-serializer.interceptor';
|
||||
import { ExecutionContext, CallHandler } from '../../interfaces';
|
||||
import { StreamableFile } from '../../file-stream';
|
||||
|
||||
describe('ClassSerializerInterceptor', () => {
|
||||
let interceptor: ClassSerializerInterceptor;
|
||||
let mockReflector: any;
|
||||
let mockTransformerPackage: any;
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox();
|
||||
mockReflector = {
|
||||
getAllAndOverride: sandbox.stub(),
|
||||
};
|
||||
mockTransformerPackage = {
|
||||
classToPlain: sandbox.stub(),
|
||||
plainToInstance: sandbox.stub(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create interceptor with default transformer package', () => {
|
||||
// This would normally load 'class-transformer' package
|
||||
// For testing, we pass a mock transformer package
|
||||
const options = {
|
||||
transformerPackage: mockTransformerPackage,
|
||||
};
|
||||
|
||||
interceptor = new ClassSerializerInterceptor(mockReflector, options);
|
||||
|
||||
expect(interceptor).to.be.instanceOf(ClassSerializerInterceptor);
|
||||
});
|
||||
|
||||
it('should use provided transformer package from options', () => {
|
||||
const customTransformer = {
|
||||
classToPlain: sandbox.stub(),
|
||||
plainToInstance: sandbox.stub(),
|
||||
};
|
||||
|
||||
const options = {
|
||||
transformerPackage: customTransformer,
|
||||
};
|
||||
|
||||
interceptor = new ClassSerializerInterceptor(mockReflector, options);
|
||||
|
||||
expect(interceptor).to.be.instanceOf(ClassSerializerInterceptor);
|
||||
});
|
||||
});
|
||||
|
||||
describe('intercept', () => {
|
||||
let mockExecutionContext: ExecutionContext;
|
||||
let mockCallHandler: CallHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
interceptor = new ClassSerializerInterceptor(mockReflector, {
|
||||
transformerPackage: mockTransformerPackage,
|
||||
});
|
||||
|
||||
mockExecutionContext = {
|
||||
getHandler: sandbox.stub(),
|
||||
getClass: sandbox.stub(),
|
||||
} as any;
|
||||
|
||||
mockCallHandler = {
|
||||
handle: sandbox.stub(),
|
||||
} as any;
|
||||
});
|
||||
|
||||
it('should transform plain object response', done => {
|
||||
const response = { id: 1, name: 'Test' };
|
||||
const transformedResponse = { id: 1, name: 'Test' };
|
||||
|
||||
mockReflector.getAllAndOverride.returns(undefined);
|
||||
mockTransformerPackage.classToPlain.returns(transformedResponse);
|
||||
(mockCallHandler.handle as sinon.SinonStub).returns(of(response));
|
||||
|
||||
interceptor
|
||||
.intercept(mockExecutionContext, mockCallHandler)
|
||||
.subscribe(result => {
|
||||
expect(result).to.equal(transformedResponse);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform array of objects', done => {
|
||||
const response = [
|
||||
{ id: 1, name: 'Test1' },
|
||||
{ id: 2, name: 'Test2' },
|
||||
];
|
||||
const transformedItem1 = { id: 1, name: 'Test1' };
|
||||
const transformedItem2 = { id: 2, name: 'Test2' };
|
||||
|
||||
mockReflector.getAllAndOverride.returns(undefined);
|
||||
mockTransformerPackage.classToPlain
|
||||
.onFirstCall()
|
||||
.returns(transformedItem1);
|
||||
mockTransformerPackage.classToPlain
|
||||
.onSecondCall()
|
||||
.returns(transformedItem2);
|
||||
(mockCallHandler.handle as sinon.SinonStub).returns(of(response));
|
||||
|
||||
interceptor
|
||||
.intercept(mockExecutionContext, mockCallHandler)
|
||||
.subscribe(result => {
|
||||
expect(result).to.be.an('array').with.lengthOf(2);
|
||||
expect(result[0]).to.equal(transformedItem1);
|
||||
expect(result[1]).to.equal(transformedItem2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge context options with default options', done => {
|
||||
const response = { id: 1, name: 'Test' };
|
||||
const defaultOptions = { excludeExtraneousValues: true };
|
||||
const contextOptions = { groups: ['user'] };
|
||||
const transformedResponse = { id: 1 };
|
||||
|
||||
interceptor = new ClassSerializerInterceptor(mockReflector, {
|
||||
transformerPackage: mockTransformerPackage,
|
||||
...defaultOptions,
|
||||
});
|
||||
|
||||
mockReflector.getAllAndOverride.returns(contextOptions);
|
||||
mockTransformerPackage.classToPlain.returns(transformedResponse);
|
||||
(mockCallHandler.handle as sinon.SinonStub).returns(of(response));
|
||||
|
||||
interceptor
|
||||
.intercept(mockExecutionContext, mockCallHandler)
|
||||
.subscribe(result => {
|
||||
const callArgs = mockTransformerPackage.classToPlain.getCall(0).args;
|
||||
expect(callArgs[1]).to.deep.include(defaultOptions);
|
||||
expect(callArgs[1]).to.deep.include(contextOptions);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call reflector with handler and class', done => {
|
||||
const response = { id: 1 };
|
||||
const mockHandler = {};
|
||||
const mockClass = {};
|
||||
|
||||
(mockExecutionContext.getHandler as sinon.SinonStub).returns(mockHandler);
|
||||
(mockExecutionContext.getClass as sinon.SinonStub).returns(mockClass);
|
||||
mockReflector.getAllAndOverride.returns(undefined);
|
||||
mockTransformerPackage.classToPlain.returns(response);
|
||||
(mockCallHandler.handle as sinon.SinonStub).returns(of(response));
|
||||
|
||||
interceptor
|
||||
.intercept(mockExecutionContext, mockCallHandler)
|
||||
.subscribe(() => {
|
||||
expect(mockReflector.getAllAndOverride.calledOnce).to.be.true;
|
||||
const args = mockReflector.getAllAndOverride.getCall(0).args;
|
||||
expect(args[1]).to.deep.equal([mockHandler, mockClass]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serialize', () => {
|
||||
beforeEach(() => {
|
||||
interceptor = new ClassSerializerInterceptor(mockReflector, {
|
||||
transformerPackage: mockTransformerPackage,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return primitive values unchanged', () => {
|
||||
expect(interceptor.serialize('string' as any, {})).to.equal('string');
|
||||
expect(interceptor.serialize(123 as any, {})).to.equal(123);
|
||||
expect(interceptor.serialize(true as any, {})).to.equal(true);
|
||||
});
|
||||
|
||||
it('should return null unchanged', () => {
|
||||
expect(interceptor.serialize(null as any, {})).to.be.null;
|
||||
});
|
||||
|
||||
it('should return undefined unchanged', () => {
|
||||
expect(interceptor.serialize(undefined as any, {})).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return StreamableFile unchanged', () => {
|
||||
const streamableFile = new StreamableFile(Buffer.from('test'));
|
||||
const result = interceptor.serialize(streamableFile as any, {});
|
||||
|
||||
expect(result).to.equal(streamableFile);
|
||||
expect(mockTransformerPackage.classToPlain.called).to.be.false;
|
||||
});
|
||||
|
||||
it('should transform plain object', () => {
|
||||
const input = { id: 1, name: 'Test' };
|
||||
const output = { id: 1 };
|
||||
|
||||
mockTransformerPackage.classToPlain.returns(output);
|
||||
|
||||
const result = interceptor.serialize(input, {});
|
||||
|
||||
expect(result).to.equal(output);
|
||||
expect(mockTransformerPackage.classToPlain.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should transform array of objects', () => {
|
||||
const input = [{ id: 1 }, { id: 2 }];
|
||||
const output1 = { id: 1 };
|
||||
const output2 = { id: 2 };
|
||||
|
||||
mockTransformerPackage.classToPlain.onFirstCall().returns(output1);
|
||||
mockTransformerPackage.classToPlain.onSecondCall().returns(output2);
|
||||
|
||||
const result = interceptor.serialize(input, {});
|
||||
|
||||
expect(result).to.be.an('array').with.lengthOf(2);
|
||||
expect(result[0]).to.equal(output1);
|
||||
expect(result[1]).to.equal(output2);
|
||||
expect(mockTransformerPackage.classToPlain.calledTwice).to.be.true;
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const input: any[] = [];
|
||||
|
||||
const result = interceptor.serialize(input, {});
|
||||
|
||||
expect(result).to.be.an('array').that.is.empty;
|
||||
expect(mockTransformerPackage.classToPlain.called).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformToPlain', () => {
|
||||
beforeEach(() => {
|
||||
interceptor = new ClassSerializerInterceptor(mockReflector, {
|
||||
transformerPackage: mockTransformerPackage,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return falsy values unchanged', () => {
|
||||
expect(interceptor.transformToPlain(null, {})).to.be.null;
|
||||
expect(interceptor.transformToPlain(undefined, {})).to.be.undefined;
|
||||
expect(interceptor.transformToPlain(0 as any, {})).to.equal(0);
|
||||
expect(interceptor.transformToPlain(false as any, {})).to.be.false;
|
||||
expect(interceptor.transformToPlain('' as any, {})).to.equal('');
|
||||
});
|
||||
|
||||
it('should use classToPlain when no type option provided', () => {
|
||||
const input = { id: 1, name: 'Test' };
|
||||
const output = { id: 1 };
|
||||
|
||||
mockTransformerPackage.classToPlain.returns(output);
|
||||
|
||||
const result = interceptor.transformToPlain(input, {});
|
||||
|
||||
expect(result).to.equal(output);
|
||||
expect(mockTransformerPackage.classToPlain.calledOnceWith(input, {})).to
|
||||
.be.true;
|
||||
expect(mockTransformerPackage.plainToInstance.called).to.be.false;
|
||||
});
|
||||
|
||||
it('should use classToPlain when input is instance of options.type', () => {
|
||||
class UserDto {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const input = new UserDto();
|
||||
input.id = 1;
|
||||
input.name = 'Test';
|
||||
|
||||
const output = { id: 1 };
|
||||
const options = { type: UserDto };
|
||||
|
||||
mockTransformerPackage.classToPlain.returns(output);
|
||||
|
||||
const result = interceptor.transformToPlain(input, options);
|
||||
|
||||
expect(result).to.equal(output);
|
||||
expect(mockTransformerPackage.classToPlain.calledOnce).to.be.true;
|
||||
expect(mockTransformerPackage.plainToInstance.called).to.be.false;
|
||||
});
|
||||
|
||||
it('should convert plain to instance then to plain when type provided but not matching', () => {
|
||||
class UserDto {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const plainInput = { id: 1, name: 'Test', password: 'secret' };
|
||||
const instanceOutput = new UserDto();
|
||||
instanceOutput.id = 1;
|
||||
|
||||
const finalOutput = { id: 1 };
|
||||
const options = { type: UserDto };
|
||||
|
||||
mockTransformerPackage.plainToInstance.returns(instanceOutput);
|
||||
mockTransformerPackage.classToPlain.returns(finalOutput);
|
||||
|
||||
const result = interceptor.transformToPlain(plainInput, options);
|
||||
|
||||
expect(result).to.equal(finalOutput);
|
||||
expect(
|
||||
mockTransformerPackage.plainToInstance.calledOnceWith(
|
||||
UserDto,
|
||||
plainInput,
|
||||
options,
|
||||
),
|
||||
).to.be.true;
|
||||
expect(
|
||||
mockTransformerPackage.classToPlain.calledOnceWith(
|
||||
instanceOutput,
|
||||
options,
|
||||
),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should handle complex nested objects', () => {
|
||||
const input = {
|
||||
user: { id: 1, name: 'Test' },
|
||||
posts: [{ id: 1, title: 'Post 1' }],
|
||||
};
|
||||
const output = {
|
||||
user: { id: 1 },
|
||||
posts: [{ id: 1 }],
|
||||
};
|
||||
|
||||
mockTransformerPackage.classToPlain.returns(output);
|
||||
|
||||
const result = interceptor.transformToPlain(input, {});
|
||||
|
||||
expect(result).to.equal(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContextOptions', () => {
|
||||
beforeEach(() => {
|
||||
interceptor = new ClassSerializerInterceptor(mockReflector, {
|
||||
transformerPackage: mockTransformerPackage,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call reflector getAllAndOverride with correct arguments', () => {
|
||||
const mockHandler = {};
|
||||
const mockClass = {};
|
||||
const mockContext = {
|
||||
getHandler: sandbox.stub().returns(mockHandler),
|
||||
getClass: sandbox.stub().returns(mockClass),
|
||||
} as any;
|
||||
|
||||
const expectedOptions = { groups: ['admin'] };
|
||||
mockReflector.getAllAndOverride.returns(expectedOptions);
|
||||
|
||||
const result = (interceptor as any).getContextOptions(mockContext);
|
||||
|
||||
expect(mockReflector.getAllAndOverride.calledOnce).to.be.true;
|
||||
const callArgs = mockReflector.getAllAndOverride.getCall(0).args;
|
||||
expect(callArgs[1]).to.deep.equal([mockHandler, mockClass]);
|
||||
expect(result).to.equal(expectedOptions);
|
||||
});
|
||||
|
||||
it('should return undefined when no metadata exists', () => {
|
||||
const mockContext = {
|
||||
getHandler: sandbox.stub().returns({}),
|
||||
getClass: sandbox.stub().returns({}),
|
||||
} as any;
|
||||
|
||||
mockReflector.getAllAndOverride.returns(undefined);
|
||||
|
||||
const result = (interceptor as any).getContextOptions(mockContext);
|
||||
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should respect handler metadata over class metadata', () => {
|
||||
const mockHandler = {};
|
||||
const mockClass = {};
|
||||
const mockContext = {
|
||||
getHandler: sandbox.stub().returns(mockHandler),
|
||||
getClass: sandbox.stub().returns(mockClass),
|
||||
} as any;
|
||||
|
||||
// getAllAndOverride should merge with handler taking precedence
|
||||
const handlerOptions = {
|
||||
groups: ['user'],
|
||||
excludeExtraneousValues: true,
|
||||
};
|
||||
mockReflector.getAllAndOverride.returns(handlerOptions);
|
||||
|
||||
const result = (interceptor as any).getContextOptions(mockContext);
|
||||
|
||||
expect(result).to.deep.equal(handlerOptions);
|
||||
// Verify it's checking handler first, then class
|
||||
const callArgs = mockReflector.getAllAndOverride.getCall(0).args;
|
||||
expect(callArgs[1][0]).to.equal(mockHandler);
|
||||
expect(callArgs[1][1]).to.equal(mockClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
beforeEach(() => {
|
||||
interceptor = new ClassSerializerInterceptor(mockReflector, {
|
||||
transformerPackage: mockTransformerPackage,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle array with mixed types', () => {
|
||||
const input = [
|
||||
{ id: 1, name: 'Test' },
|
||||
null,
|
||||
undefined,
|
||||
{ id: 2, name: 'Test2' },
|
||||
];
|
||||
|
||||
mockTransformerPackage.classToPlain
|
||||
.onCall(0)
|
||||
.returns({ id: 1, name: 'Test' });
|
||||
mockTransformerPackage.classToPlain.onCall(1).returns(null);
|
||||
mockTransformerPackage.classToPlain.onCall(2).returns(undefined);
|
||||
mockTransformerPackage.classToPlain
|
||||
.onCall(3)
|
||||
.returns({ id: 2, name: 'Test2' });
|
||||
|
||||
const result = interceptor.serialize(input, {});
|
||||
|
||||
expect(result).to.be.an('array').with.lengthOf(4);
|
||||
expect(result[1]).to.be.null;
|
||||
expect(result[2]).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should not transform when response is not an object', () => {
|
||||
const input = 'plain string';
|
||||
|
||||
const result = interceptor.serialize(input as any, {});
|
||||
|
||||
expect(result).to.equal(input);
|
||||
expect(mockTransformerPackage.classToPlain.called).to.be.false;
|
||||
});
|
||||
|
||||
it('should handle Date objects', () => {
|
||||
const date = new Date();
|
||||
const output = { date: date.toISOString() };
|
||||
|
||||
mockTransformerPackage.classToPlain.returns(output);
|
||||
|
||||
const result = interceptor.serialize({ date } as any, {});
|
||||
|
||||
expect(mockTransformerPackage.classToPlain.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should pass through options to transformer', () => {
|
||||
const input = { id: 1, name: 'Test', password: 'secret' };
|
||||
const options = {
|
||||
excludeExtraneousValues: true,
|
||||
groups: ['public'],
|
||||
strategy: 'excludeAll',
|
||||
};
|
||||
|
||||
mockTransformerPackage.classToPlain.returns({ id: 1, name: 'Test' });
|
||||
|
||||
interceptor.transformToPlain(input, options as any);
|
||||
|
||||
expect(mockTransformerPackage.classToPlain.calledOnce).to.be.true;
|
||||
const callArgs = mockTransformerPackage.classToPlain.getCall(0).args;
|
||||
expect(callArgs[1]).to.deep.include(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
539
packages/core/test/discovery/discovery-service.spec.ts
Normal file
539
packages/core/test/discovery/discovery-service.spec.ts
Normal file
@@ -0,0 +1,539 @@
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { DiscoveryService } from '../../discovery/discovery-service';
|
||||
import { InstanceWrapper } from '../../injector/instance-wrapper';
|
||||
import { Module } from '../../injector/module';
|
||||
import { ModulesContainer } from '../../injector/modules-container';
|
||||
import { DiscoverableMetaHostCollection } from '../../discovery/discoverable-meta-host-collection';
|
||||
|
||||
describe('DiscoveryService', () => {
|
||||
let discoveryService: DiscoveryService;
|
||||
let modulesContainer: ModulesContainer;
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
|
||||
beforeEach(() => {
|
||||
modulesContainer = new ModulesContainer();
|
||||
discoveryService = new DiscoveryService(modulesContainer);
|
||||
sandbox = sinon.createSandbox();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('createDecorator', () => {
|
||||
it('should create a decorator with a unique KEY', () => {
|
||||
const decorator1 = DiscoveryService.createDecorator();
|
||||
const decorator2 = DiscoveryService.createDecorator();
|
||||
|
||||
expect(decorator1.KEY).to.be.a('string');
|
||||
expect(decorator2.KEY).to.be.a('string');
|
||||
expect(decorator1.KEY).to.not.equal(decorator2.KEY);
|
||||
});
|
||||
|
||||
it('should create a decorator that can decorate classes', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator<{
|
||||
value: string;
|
||||
}>();
|
||||
|
||||
@TestDecorator({ value: 'test' })
|
||||
class TestClass {}
|
||||
|
||||
const metadata = Reflect.getMetadata(TestDecorator.KEY, TestClass);
|
||||
expect(metadata).to.deep.equal({ value: 'test' });
|
||||
});
|
||||
|
||||
it('should create a decorator that can decorate methods', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator<{
|
||||
event: string;
|
||||
}>();
|
||||
|
||||
class TestClass {
|
||||
@TestDecorator({ event: 'click' })
|
||||
handleClick() {}
|
||||
}
|
||||
|
||||
const metadata = Reflect.getMetadata(
|
||||
TestDecorator.KEY,
|
||||
new TestClass().handleClick,
|
||||
);
|
||||
expect(metadata).to.deep.equal({ event: 'click' });
|
||||
});
|
||||
|
||||
it('should use empty object as default metadata when no options provided', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator();
|
||||
|
||||
@TestDecorator()
|
||||
class TestClass {}
|
||||
|
||||
const metadata = Reflect.getMetadata(TestDecorator.KEY, TestClass);
|
||||
expect(metadata).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('should add class to DiscoverableMetaHostCollection when decorating a class', () => {
|
||||
const addClassMetaHostLinkSpy = sandbox.spy(
|
||||
DiscoverableMetaHostCollection,
|
||||
'addClassMetaHostLink',
|
||||
);
|
||||
const TestDecorator = DiscoveryService.createDecorator();
|
||||
|
||||
@TestDecorator()
|
||||
class TestClass {}
|
||||
|
||||
expect(addClassMetaHostLinkSpy.calledOnce).to.be.true;
|
||||
expect(addClassMetaHostLinkSpy.calledWith(TestClass, TestDecorator.KEY))
|
||||
.to.be.true;
|
||||
});
|
||||
|
||||
it('should not add to DiscoverableMetaHostCollection when decorating a method', () => {
|
||||
const addClassMetaHostLinkSpy = sandbox.spy(
|
||||
DiscoverableMetaHostCollection,
|
||||
'addClassMetaHostLink',
|
||||
);
|
||||
const TestDecorator = DiscoveryService.createDecorator();
|
||||
|
||||
class TestClass {
|
||||
@TestDecorator()
|
||||
testMethod() {}
|
||||
}
|
||||
|
||||
expect(addClassMetaHostLinkSpy.called).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviders', () => {
|
||||
it('should return all providers from all modules when no options provided', () => {
|
||||
const module1 = new Module(class Module1 {}, modulesContainer as any);
|
||||
const module2 = new Module(class Module2 {}, modulesContainer as any);
|
||||
|
||||
// Clear default providers
|
||||
module1.providers.clear();
|
||||
module2.providers.clear();
|
||||
|
||||
const provider1 = new InstanceWrapper({
|
||||
name: 'Provider1',
|
||||
token: 'PROVIDER_1',
|
||||
});
|
||||
const provider2 = new InstanceWrapper({
|
||||
name: 'Provider2',
|
||||
token: 'PROVIDER_2',
|
||||
});
|
||||
const provider3 = new InstanceWrapper({
|
||||
name: 'Provider3',
|
||||
token: 'PROVIDER_3',
|
||||
});
|
||||
|
||||
module1.providers.set('PROVIDER_1', provider1);
|
||||
module1.providers.set('PROVIDER_2', provider2);
|
||||
module2.providers.set('PROVIDER_3', provider3);
|
||||
|
||||
modulesContainer.set('Module1', module1);
|
||||
modulesContainer.set('Module2', module2);
|
||||
|
||||
const providers = discoveryService.getProviders();
|
||||
|
||||
expect(providers).to.have.lengthOf(3);
|
||||
expect(providers).to.include(provider1);
|
||||
expect(providers).to.include(provider2);
|
||||
expect(providers).to.include(provider3);
|
||||
});
|
||||
|
||||
it('should return empty array when no modules exist', () => {
|
||||
const providers = discoveryService.getProviders();
|
||||
expect(providers).to.be.an('array').that.is.empty;
|
||||
});
|
||||
|
||||
it('should return empty array when modules have no providers', () => {
|
||||
const module1 = new Module(class Module1 {}, modulesContainer as any);
|
||||
module1.providers.clear();
|
||||
modulesContainer.set('Module1', module1);
|
||||
|
||||
const providers = discoveryService.getProviders();
|
||||
expect(providers).to.be.an('array').that.is.empty;
|
||||
});
|
||||
|
||||
it('should filter providers by metadataKey when provided', () => {
|
||||
const metadataKey = 'test-metadata-key';
|
||||
const provider1 = new InstanceWrapper({
|
||||
name: 'Provider1',
|
||||
token: 'PROVIDER_1',
|
||||
});
|
||||
const provider2 = new InstanceWrapper({
|
||||
name: 'Provider2',
|
||||
token: 'PROVIDER_2',
|
||||
});
|
||||
|
||||
const providerSet = new Set([provider1, provider2]);
|
||||
const getProvidersByMetaKeyStub = sandbox
|
||||
.stub(DiscoverableMetaHostCollection, 'getProvidersByMetaKey')
|
||||
.returns(providerSet);
|
||||
|
||||
const providers = discoveryService.getProviders({ metadataKey });
|
||||
|
||||
expect(getProvidersByMetaKeyStub.calledOnce).to.be.true;
|
||||
expect(
|
||||
getProvidersByMetaKeyStub.calledWith(modulesContainer, metadataKey),
|
||||
).to.be.true;
|
||||
expect(providers).to.have.lengthOf(2);
|
||||
expect(providers).to.include(provider1);
|
||||
expect(providers).to.include(provider2);
|
||||
});
|
||||
|
||||
it('should return empty array when no providers match the metadataKey', () => {
|
||||
const metadataKey = 'non-existent-key';
|
||||
const emptySet = new Set<InstanceWrapper>();
|
||||
|
||||
sandbox
|
||||
.stub(DiscoverableMetaHostCollection, 'getProvidersByMetaKey')
|
||||
.returns(emptySet);
|
||||
|
||||
const providers = discoveryService.getProviders({ metadataKey });
|
||||
expect(providers).to.be.an('array').that.is.empty;
|
||||
});
|
||||
|
||||
it('should filter providers by included modules', () => {
|
||||
class Module1 {}
|
||||
class Module2 {}
|
||||
class Module3 {}
|
||||
|
||||
const module1 = new Module(Module1, modulesContainer as any);
|
||||
const module2 = new Module(Module2, modulesContainer as any);
|
||||
const module3 = new Module(Module3, modulesContainer as any);
|
||||
|
||||
// Clear default providers
|
||||
module1.providers.clear();
|
||||
module2.providers.clear();
|
||||
module3.providers.clear();
|
||||
|
||||
const provider1 = new InstanceWrapper({
|
||||
name: 'Provider1',
|
||||
token: 'PROVIDER_1',
|
||||
});
|
||||
const provider2 = new InstanceWrapper({
|
||||
name: 'Provider2',
|
||||
token: 'PROVIDER_2',
|
||||
});
|
||||
const provider3 = new InstanceWrapper({
|
||||
name: 'Provider3',
|
||||
token: 'PROVIDER_3',
|
||||
});
|
||||
|
||||
module1.providers.set('PROVIDER_1', provider1);
|
||||
module2.providers.set('PROVIDER_2', provider2);
|
||||
module3.providers.set('PROVIDER_3', provider3);
|
||||
|
||||
modulesContainer.set('Module1', module1);
|
||||
modulesContainer.set('Module2', module2);
|
||||
modulesContainer.set('Module3', module3);
|
||||
|
||||
const providers = discoveryService.getProviders({
|
||||
include: [Module1, Module2],
|
||||
});
|
||||
|
||||
expect(providers).to.have.lengthOf(2);
|
||||
expect(providers).to.include(provider1);
|
||||
expect(providers).to.include(provider2);
|
||||
expect(providers).to.not.include(provider3);
|
||||
});
|
||||
|
||||
it('should return empty array when include option is empty array', () => {
|
||||
const module1 = new Module(class Module1 {}, modulesContainer as any);
|
||||
const provider1 = new InstanceWrapper({
|
||||
name: 'Provider1',
|
||||
token: 'PROVIDER_1',
|
||||
});
|
||||
module1.providers.set('PROVIDER_1', provider1);
|
||||
modulesContainer.set('Module1', module1);
|
||||
|
||||
const providers = discoveryService.getProviders({ include: [] });
|
||||
expect(providers).to.be.an('array').that.is.empty;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getControllers', () => {
|
||||
it('should return all controllers from all modules when no options provided', () => {
|
||||
const module1 = new Module(class Module1 {}, modulesContainer as any);
|
||||
const module2 = new Module(class Module2 {}, modulesContainer as any);
|
||||
|
||||
const controller1 = new InstanceWrapper({
|
||||
name: 'Controller1',
|
||||
token: 'CONTROLLER_1',
|
||||
});
|
||||
const controller2 = new InstanceWrapper({
|
||||
name: 'Controller2',
|
||||
token: 'CONTROLLER_2',
|
||||
});
|
||||
|
||||
module1.controllers.set('CONTROLLER_1', controller1);
|
||||
module2.controllers.set('CONTROLLER_2', controller2);
|
||||
|
||||
modulesContainer.set('Module1', module1);
|
||||
modulesContainer.set('Module2', module2);
|
||||
|
||||
const controllers = discoveryService.getControllers();
|
||||
|
||||
expect(controllers).to.have.lengthOf(2);
|
||||
expect(controllers).to.include(controller1);
|
||||
expect(controllers).to.include(controller2);
|
||||
});
|
||||
|
||||
it('should return empty array when no controllers exist', () => {
|
||||
const module1 = new Module(class Module1 {}, modulesContainer as any);
|
||||
modulesContainer.set('Module1', module1);
|
||||
|
||||
const controllers = discoveryService.getControllers();
|
||||
expect(controllers).to.be.an('array').that.is.empty;
|
||||
});
|
||||
|
||||
it('should filter controllers by metadataKey when provided', () => {
|
||||
const metadataKey = 'controller-metadata-key';
|
||||
const controller1 = new InstanceWrapper({
|
||||
name: 'Controller1',
|
||||
token: 'CONTROLLER_1',
|
||||
});
|
||||
|
||||
const controllerSet = new Set([controller1]);
|
||||
const getControllersByMetaKeyStub = sandbox
|
||||
.stub(DiscoverableMetaHostCollection, 'getControllersByMetaKey')
|
||||
.returns(controllerSet);
|
||||
|
||||
const controllers = discoveryService.getControllers({ metadataKey });
|
||||
|
||||
expect(getControllersByMetaKeyStub.calledOnce).to.be.true;
|
||||
expect(
|
||||
getControllersByMetaKeyStub.calledWith(modulesContainer, metadataKey),
|
||||
).to.be.true;
|
||||
expect(controllers).to.have.lengthOf(1);
|
||||
expect(controllers).to.include(controller1);
|
||||
});
|
||||
|
||||
it('should filter controllers by included modules', () => {
|
||||
class Module1 {}
|
||||
class Module2 {}
|
||||
|
||||
const module1 = new Module(Module1, modulesContainer as any);
|
||||
const module2 = new Module(Module2, modulesContainer as any);
|
||||
|
||||
const controller1 = new InstanceWrapper({
|
||||
name: 'Controller1',
|
||||
token: 'CONTROLLER_1',
|
||||
});
|
||||
const controller2 = new InstanceWrapper({
|
||||
name: 'Controller2',
|
||||
token: 'CONTROLLER_2',
|
||||
});
|
||||
|
||||
module1.controllers.set('CONTROLLER_1', controller1);
|
||||
module2.controllers.set('CONTROLLER_2', controller2);
|
||||
|
||||
modulesContainer.set('Module1', module1);
|
||||
modulesContainer.set('Module2', module2);
|
||||
|
||||
const controllers = discoveryService.getControllers({
|
||||
include: [Module1],
|
||||
});
|
||||
|
||||
expect(controllers).to.have.lengthOf(1);
|
||||
expect(controllers).to.include(controller1);
|
||||
expect(controllers).to.not.include(controller2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMetadataByDecorator', () => {
|
||||
it('should retrieve metadata from class using decorator', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator<{
|
||||
role: string;
|
||||
}>();
|
||||
|
||||
@TestDecorator({ role: 'admin' })
|
||||
class TestClass {}
|
||||
|
||||
const instance = new TestClass();
|
||||
const wrapper = new InstanceWrapper({
|
||||
name: 'TestClass',
|
||||
token: TestClass,
|
||||
metatype: TestClass,
|
||||
instance,
|
||||
});
|
||||
|
||||
const metadata = discoveryService.getMetadataByDecorator(
|
||||
TestDecorator,
|
||||
wrapper,
|
||||
);
|
||||
|
||||
expect(metadata).to.deep.equal({ role: 'admin' });
|
||||
});
|
||||
|
||||
it('should retrieve metadata from method using decorator and methodKey', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator<{
|
||||
event: string;
|
||||
}>();
|
||||
|
||||
class TestClass {
|
||||
@TestDecorator({ event: 'created' })
|
||||
onCreate() {}
|
||||
}
|
||||
|
||||
const instance = new TestClass();
|
||||
const wrapper = new InstanceWrapper({
|
||||
name: 'TestClass',
|
||||
token: TestClass,
|
||||
metatype: TestClass,
|
||||
instance,
|
||||
});
|
||||
|
||||
const metadata = discoveryService.getMetadataByDecorator(
|
||||
TestDecorator,
|
||||
wrapper,
|
||||
'onCreate',
|
||||
);
|
||||
|
||||
expect(metadata).to.deep.equal({ event: 'created' });
|
||||
});
|
||||
|
||||
it('should return undefined when metadata does not exist', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator();
|
||||
|
||||
class TestClass {}
|
||||
|
||||
const instance = new TestClass();
|
||||
const wrapper = new InstanceWrapper({
|
||||
name: 'TestClass',
|
||||
token: TestClass,
|
||||
metatype: TestClass,
|
||||
instance,
|
||||
});
|
||||
|
||||
const metadata = discoveryService.getMetadataByDecorator(
|
||||
TestDecorator,
|
||||
wrapper,
|
||||
);
|
||||
|
||||
expect(metadata).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return undefined when methodKey does not exist on instance', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator();
|
||||
|
||||
class TestClass {
|
||||
existingMethod() {}
|
||||
}
|
||||
|
||||
const instance = new TestClass();
|
||||
const wrapper = new InstanceWrapper({
|
||||
name: 'TestClass',
|
||||
token: TestClass,
|
||||
metatype: TestClass,
|
||||
instance,
|
||||
});
|
||||
|
||||
const metadata = discoveryService.getMetadataByDecorator(
|
||||
TestDecorator,
|
||||
wrapper,
|
||||
'existingMethod', // Use existing method to avoid undefined reference error
|
||||
);
|
||||
|
||||
expect(metadata).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should use metatype when instance.constructor is undefined', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator<{
|
||||
value: number;
|
||||
}>();
|
||||
|
||||
@TestDecorator({ value: 42 })
|
||||
class TestClass {}
|
||||
|
||||
const instanceWithoutConstructor: any = Object.create(null);
|
||||
|
||||
const wrapper = new InstanceWrapper({
|
||||
name: 'TestClass',
|
||||
token: TestClass,
|
||||
metatype: TestClass,
|
||||
instance: instanceWithoutConstructor,
|
||||
});
|
||||
|
||||
const metadata = discoveryService.getMetadataByDecorator(
|
||||
TestDecorator,
|
||||
wrapper,
|
||||
);
|
||||
|
||||
expect(metadata).to.deep.equal({ value: 42 });
|
||||
});
|
||||
|
||||
it('should handle undefined instance gracefully', () => {
|
||||
const TestDecorator = DiscoveryService.createDecorator<{
|
||||
test: string;
|
||||
}>();
|
||||
|
||||
@TestDecorator({ test: 'value' })
|
||||
class TestClass {}
|
||||
|
||||
const wrapper = new InstanceWrapper({
|
||||
name: 'TestClass',
|
||||
token: TestClass,
|
||||
metatype: TestClass,
|
||||
instance: undefined,
|
||||
});
|
||||
|
||||
const metadata = discoveryService.getMetadataByDecorator(
|
||||
TestDecorator,
|
||||
wrapper,
|
||||
);
|
||||
|
||||
expect(metadata).to.deep.equal({ test: 'value' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModules', () => {
|
||||
it('should return all modules when no options provided', () => {
|
||||
const module1 = new Module(class Module1 {}, modulesContainer as any);
|
||||
const module2 = new Module(class Module2 {}, modulesContainer as any);
|
||||
|
||||
modulesContainer.set('Module1', module1);
|
||||
modulesContainer.set('Module2', module2);
|
||||
|
||||
const modules = (discoveryService as any).getModules();
|
||||
|
||||
expect(modules).to.have.lengthOf(2);
|
||||
expect(modules).to.include(module1);
|
||||
expect(modules).to.include(module2);
|
||||
});
|
||||
|
||||
it('should return empty array when no modules exist', () => {
|
||||
const modules = (discoveryService as any).getModules();
|
||||
expect(modules).to.be.an('array').that.is.empty;
|
||||
});
|
||||
|
||||
it('should filter modules by include option', () => {
|
||||
class Module1 {}
|
||||
class Module2 {}
|
||||
class Module3 {}
|
||||
|
||||
const module1 = new Module(Module1, modulesContainer as any);
|
||||
const module2 = new Module(Module2, modulesContainer as any);
|
||||
const module3 = new Module(Module3, modulesContainer as any);
|
||||
|
||||
modulesContainer.set('Module1', module1);
|
||||
modulesContainer.set('Module2', module2);
|
||||
modulesContainer.set('Module3', module3);
|
||||
|
||||
const modules = (discoveryService as any).getModules({
|
||||
include: [Module1, Module3],
|
||||
});
|
||||
|
||||
expect(modules).to.have.lengthOf(2);
|
||||
expect(modules).to.include(module1);
|
||||
expect(modules).to.include(module3);
|
||||
expect(modules).to.not.include(module2);
|
||||
});
|
||||
|
||||
it('should return empty array when include option is empty', () => {
|
||||
const module1 = new Module(class Module1 {}, modulesContainer as any);
|
||||
modulesContainer.set('Module1', module1);
|
||||
|
||||
const modules = (discoveryService as any).getModules({ include: [] });
|
||||
expect(modules).to.be.an('array').that.is.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user