UNPKG

@feature-hub/core

Version:

Create scalable web applications using micro frontends.

542 lines 28.9 kB
// tslint:disable:no-implicit-dependencies import { FeatureServiceRegistry, } from '..'; import { logger } from './logger'; describe('FeatureServiceRegistry', () => { let featureServiceRegistry; let mockExternalsValidator; let providerDefinitionA; let providerDefinitionB; let providerDefinitionC; let binderA; let binderB; let binderC; let bindingA; let bindingB; let bindingC; let featureServiceA; let featureServiceB; let featureServiceC; beforeEach(() => { mockExternalsValidator = { validate: jest.fn(), }; featureServiceRegistry = new FeatureServiceRegistry({ logger }); featureServiceA = { kind: 'featureServiceA' }; bindingA = { featureService: featureServiceA }; binderA = jest.fn(() => bindingA); providerDefinitionA = { id: 'a', create: jest.fn(() => ({ '1.1.0': binderA })), }; featureServiceB = { kind: 'featureServiceB' }; bindingB = { featureService: featureServiceB }; binderB = jest.fn(() => bindingB); providerDefinitionB = { id: 'b', optionalDependencies: { featureServices: { a: '^1.0.0' } }, create: jest.fn(() => ({ '1.0.0': binderB })), }; featureServiceC = { kind: 'featureServiceC' }; bindingC = { featureService: featureServiceC }; binderC = jest.fn(() => bindingC); providerDefinitionC = { id: 'c', dependencies: { featureServices: { a: '^1.0.0', b: '1.0.0' } }, create: jest.fn(() => ({ '2.0.0': binderC })), }; }); describe('#registerFeatureServices', () => { function testRegistrationOrderABC() { expect(providerDefinitionA.create.mock.calls).toEqual([ [{ featureServices: {} }], ]); expect(binderA.mock.calls).toEqual([ ['b', undefined], ['c', undefined], ]); expect(providerDefinitionB.create.mock.calls).toEqual([ [{ featureServices: { a: featureServiceA } }], ]); expect(binderB.mock.calls).toEqual([['c', undefined]]); expect(providerDefinitionC.create.mock.calls).toEqual([ [{ featureServices: { a: featureServiceA, b: featureServiceB } }], ]); expect(binderC.mock.calls).toEqual([]); expect(logger.info.mock.calls).toEqual([ [ 'The Feature Service "a" has been successfully registered by registrant "test".', ], [ 'The required Feature Service "a" has been successfully bound to consumer "b".', ], [ 'The Feature Service "b" has been successfully registered by registrant "test".', ], [ 'The required Feature Service "a" has been successfully bound to consumer "c".', ], [ 'The required Feature Service "b" has been successfully bound to consumer "c".', ], [ 'The Feature Service "c" has been successfully registered by registrant "test".', ], ]); } beforeEach(() => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); }); it('registers the Feature Services "a", "b", "c" one after the other', () => { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); featureServiceRegistry.registerFeatureServices([providerDefinitionB], 'test'); featureServiceRegistry.registerFeatureServices([providerDefinitionC], 'test'); testRegistrationOrderABC(); }); it('registers the Feature Services "a", "b", "c" all at once in topologically sorted order', () => { featureServiceRegistry.registerFeatureServices([providerDefinitionB, providerDefinitionC, providerDefinitionA], 'test'); testRegistrationOrderABC(); }); it('does not register the already existing Feature Service "b"', () => { featureServiceRegistry.registerFeatureServices([providerDefinitionA, providerDefinitionB], 'test'); featureServiceRegistry.registerFeatureServices([providerDefinitionB], 'test'); expect(providerDefinitionB.create.mock.calls).toEqual([ [{ featureServices: { a: featureServiceA } }], ]); expect(binderA.mock.calls).toEqual([['b', undefined]]); expect(binderB.mock.calls).toEqual([]); expect(logger.info.mock.calls).toEqual([ [ 'The Feature Service "a" has been successfully registered by registrant "test".', ], [ 'The required Feature Service "a" has been successfully bound to consumer "b".', ], [ 'The Feature Service "b" has been successfully registered by registrant "test".', ], ]); expect(logger.warn.mock.calls).toEqual([ [ 'The already registered Feature Service "b" could not be re-registered by registrant "test".', ], ]); }); it('does not register a Feature Service that returns undefined from create', () => { providerDefinitionA.create.mockReturnValue(undefined); featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(providerDefinitionA.create.mock.calls).toEqual([ [{ featureServices: {} }], ]); expect(logger.info.mock.calls).toEqual([ [ 'The Feature Service "a" could not be registered by registrant "test" because it returned undefined.', ], ]); }); it('fails to register the Feature Service "c" due to the lack of dependency "a"', () => { expect(() => featureServiceRegistry.registerFeatureServices([providerDefinitionC], 'test')).toThrow(new Error('The required Feature Service "a" is not registered and therefore could not be bound to consumer "c".')); }); it('does not fail to register the Feature Service "b" due to the lack of optional dependency "a"', () => { providerDefinitionB = { id: 'b', optionalDependencies: { featureServices: { a: '^1.0.0' } }, create: jest.fn(() => ({ '1.0.0': jest.fn() })), }; expect(() => featureServiceRegistry.registerFeatureServices([providerDefinitionB], 'test')).not.toThrow(); expect(logger.info.mock.calls).toEqual([ [ 'The optional Feature Service "a" is not registered and therefore could not be bound to consumer "b".', ], [ 'The Feature Service "b" has been successfully registered by registrant "test".', ], ]); }); it('fails to register a Feature Service due to an unsupported dependency version', () => { const stateProviderD = { id: 'd', dependencies: { featureServices: { a: '^2.0.0' } }, create: jest.fn(), }; expect(() => featureServiceRegistry.registerFeatureServices([providerDefinitionA, stateProviderD], 'test')).toThrow(new Error('The required Feature Service "a" in the unsupported version range "^2.0.0" could not be bound to consumer "d". The supported versions are ["1.1.0"].')); }); it('does not fail to register a Feature Service due to an unsupported optional dependency version', () => { const stateProviderD = { id: 'd', optionalDependencies: { featureServices: { a: '^2.0.0' } }, create: jest.fn(() => ({})), }; expect(() => featureServiceRegistry.registerFeatureServices([providerDefinitionA, stateProviderD], 'test')).not.toThrow(); expect(logger.info.mock.calls).toEqual([ [ 'The Feature Service "a" has been successfully registered by registrant "test".', ], [ 'The optional Feature Service "a" in the unsupported version range "^2.0.0" could not be bound to consumer "d". The supported versions are ["1.1.0"].', ], [ 'The Feature Service "d" has been successfully registered by registrant "test".', ], ]); }); it('fails to register a Feature Service due to an invalid dependency version', () => { const stateProviderDefinitionD = { id: 'd', dependencies: { featureServices: { a: '' } }, create: jest.fn(), }; expect(() => featureServiceRegistry.registerFeatureServices([providerDefinitionA, stateProviderDefinitionD], 'test')).toThrow(new Error('The required Feature Service "a" in an invalid version could not be bound to consumer "d".')); }); it('does not fail to register a Feature Service due to an invalid optional dependency version', () => { const stateProviderDefinitionD = { id: 'd', optionalDependencies: { featureServices: { a: '' } }, create: jest.fn(() => ({})), }; expect(() => featureServiceRegistry.registerFeatureServices([providerDefinitionA, stateProviderDefinitionD], 'test')).not.toThrow(); expect(logger.info.mock.calls).toEqual([ [ 'The Feature Service "a" has been successfully registered by registrant "test".', ], [ 'The optional Feature Service "a" in an invalid version could not be bound to consumer "d".', ], [ 'The Feature Service "d" has been successfully registered by registrant "test".', ], ]); }); it('fails to register a Feature Service that provides an invalid version', () => { const stateProviderDefinitionD = { id: 'd', create: jest.fn(() => ({ '1.0.0': jest.fn(), '2.0': jest.fn() })), }; expect(() => featureServiceRegistry.registerFeatureServices([stateProviderDefinitionD], 'test')).toThrow(new Error('The Feature Service "d" could not be registered by registrant "test" because it defines the invalid version "2.0".')); }); describe('without an ExternalsValidator provided to the FeatureServiceRegistry', () => { describe('with a Feature Service definition that is declaring external dependencies', () => { beforeEach(() => { providerDefinitionA = Object.assign(Object.assign({}, providerDefinitionA), { dependencies: { externals: { react: '^16.0.0', }, } }); }); it("doesn't throw an error", () => { expect(() => { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); }).not.toThrow(); }); }); }); describe('with an ExternalsValidator provided to the FeatureServiceRegistry', () => { beforeEach(() => { featureServiceRegistry = new FeatureServiceRegistry({ externalsValidator: mockExternalsValidator, logger, }); }); describe('with a Feature Service definition that is failing the externals validation', () => { let mockError; beforeEach(() => { mockError = new Error('mockError'); mockExternalsValidator.validate = jest.fn(() => { throw mockError; }); providerDefinitionA = Object.assign(Object.assign({}, providerDefinitionA), { dependencies: { externals: { react: '^16.0.0', }, } }); }); it('calls the provided ExternalsValidator with the defined externals', () => { try { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); } catch (_a) { } expect(mockExternalsValidator.validate).toHaveBeenCalledWith({ react: '^16.0.0' }, 'a'); }); it('throws the validation error', () => { expect(() => { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); }).toThrow(mockError); }); }); describe('with a Feature Service definition that is not failing the externals validation', () => { beforeEach(() => { providerDefinitionA = Object.assign(Object.assign({}, providerDefinitionA), { dependencies: { externals: { react: '^16.0.0', }, } }); }); it('calls the provided ExternalsValidator with the defined externals', () => { try { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); } catch (_a) { } expect(mockExternalsValidator.validate).toHaveBeenCalledWith({ react: '^16.0.0' }, 'a'); }); it("doesn't throw an error", () => { expect(() => { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); }).not.toThrow(); }); }); describe('with a Feature Service definition that declares no externals', () => { it('does not call the provided ExternalsValidator', () => { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(mockExternalsValidator.validate).not.toHaveBeenCalled(); }); it("doesn't throw an error", () => { expect(() => { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); }).not.toThrow(); }); }); }); }); describe('#bindFeatureServices', () => { describe('for a Feature Service consumer without dependencies', () => { it('creates a bindings object with no Feature Services', () => { expect(featureServiceRegistry.bindFeatureServices({}, 'foo')).toEqual({ featureServices: {}, unbind: expect.any(Function), }); }); }); describe('for a Feature Service consumer with caret range dependencies', () => { it('creates a bindings object with Feature Services', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(binderA.mock.calls).toEqual([]); expect(featureServiceRegistry.bindFeatureServices({ dependencies: { featureServices: { a: '^1.0.0' } } }, 'foo')).toEqual({ featureServices: { a: featureServiceA }, unbind: expect.any(Function), }); expect(binderA.mock.calls).toEqual([['foo', undefined]]); }); }); describe('for a Feature Service consumer with tilde range dependencies', () => { it('creates a bindings object with Feature Services', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(binderA.mock.calls).toEqual([]); expect(featureServiceRegistry.bindFeatureServices({ dependencies: { featureServices: { a: '~1.0.0' } } }, 'foo')).toEqual({ featureServices: { a: featureServiceA }, unbind: expect.any(Function), }); expect(binderA.mock.calls).toEqual([['foo', undefined]]); }); }); describe('for a Feature Service consumer with exact dependencies', () => { it('creates a bindings object with Feature Services', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(binderA.mock.calls).toEqual([]); expect(featureServiceRegistry.bindFeatureServices({ dependencies: { featureServices: { a: '1.0.0' } } }, 'foo')).toEqual({ featureServices: { a: featureServiceA }, unbind: expect.any(Function), }); expect(binderA.mock.calls).toEqual([['foo', undefined]]); }); }); describe('for a Feature Service consumer and an optional dependency to a Feature Service that returned undefined in create', () => { it('creates a bindings object without Feature Services', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); providerDefinitionA.create.mockReturnValue(undefined); featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(binderA.mock.calls).toEqual([]); expect(featureServiceRegistry.bindFeatureServices({ optionalDependencies: { featureServices: { a: '1.1.0' } } }, 'foo')).toEqual({ featureServices: {}, unbind: expect.any(Function) }); expect(binderA.mock.calls).toEqual([]); }); }); describe('for a Feature Service consumer and two optional dependencies', () => { describe('with the first dependency missing', () => { it('creates a bindings object with Feature Services', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(binderA.mock.calls).toEqual([]); expect(featureServiceRegistry.bindFeatureServices({ optionalDependencies: { featureServices: { b: '1.0.0', a: '1.1.0' }, }, }, 'foo')).toEqual({ featureServices: { a: featureServiceA }, unbind: expect.any(Function), }); expect(binderA.mock.calls).toEqual([['foo', undefined]]); }); }); describe('with the second dependency missing', () => { it('creates a bindings object with Feature Services', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(binderA.mock.calls).toEqual([]); expect(featureServiceRegistry.bindFeatureServices({ optionalDependencies: { featureServices: { a: '1.1.0', b: '1.0.0' }, }, }, 'foo')).toEqual({ featureServices: { a: featureServiceA }, unbind: expect.any(Function), }); expect(binderA.mock.calls).toEqual([['foo', undefined]]); }); }); describe('with no dependency missing', () => { it('creates a bindings object with Feature Services', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); featureServiceRegistry.registerFeatureServices([providerDefinitionA, providerDefinitionB], 'test'); expect(binderA.mock.calls).toEqual([['b', undefined]]); expect(featureServiceRegistry.bindFeatureServices({ optionalDependencies: { featureServices: { a: '1.1.0', b: '^1.0.0' }, }, }, 'foo')).toEqual({ featureServices: { a: featureServiceA, b: featureServiceB }, unbind: expect.any(Function), }); expect(binderA.mock.calls).toEqual([ ['b', undefined], ['foo', undefined], ]); expect(binderB.mock.calls).toEqual([['foo', undefined]]); }); }); }); describe('when a consumerName is provided', () => { it('passes the consumerName to the Feature Service binders of all dependencies', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); featureServiceRegistry.registerFeatureServices([providerDefinitionA, providerDefinitionB], 'testId'); expect(featureServiceRegistry.bindFeatureServices({ dependencies: { featureServices: { a: '^1.0.0', b: '^1.0.0' } } }, 'testId', 'testName')).toEqual({ featureServices: { a: featureServiceA, b: featureServiceB }, unbind: expect.any(Function), }); expect(binderA.mock.calls).toEqual([ ['b', undefined], ['testId', 'testName'], ]); expect(binderB.mock.calls).toEqual([['testId', 'testName']]); }); }); it('fails to create a bindings object for a consumer which is already bound', () => { featureServiceRegistry.bindFeatureServices({}, 'foo'); expect(() => featureServiceRegistry.bindFeatureServices({}, 'foo')).toThrow(new Error('All required Feature Services are already bound to consumer "foo".')); }); describe('#unbind', () => { it('unbinds the consumer', () => { const bindings = featureServiceRegistry.bindFeatureServices(providerDefinitionA, providerDefinitionA.id); bindings.unbind(); expect(() => featureServiceRegistry.bindFeatureServices(providerDefinitionA, providerDefinitionA.id)).not.toThrow(); }); it('unbinds all consumers if applicable, errors are ignored', () => { const mockError = new Error('I should be caught.'); bindingA.unbind = jest.fn(() => { throw mockError; }); // The "bindingB" intentionally has no destroy method. bindingC.unbind = jest.fn(); featureServiceRegistry.registerFeatureServices([providerDefinitionA, providerDefinitionB, providerDefinitionC], 'test'); const bindings = featureServiceRegistry.bindFeatureServices({ dependencies: { featureServices: { a: '1.1.0', b: '1.0.0', c: '2.0.0' }, }, }, 'foo'); bindings.unbind(); expect(bindingA.unbind).toHaveBeenCalledTimes(1); expect(bindingC.unbind).toHaveBeenCalledTimes(1); expect(logger.info.mock.calls).toEqual([ [ 'The Feature Service "a" has been successfully registered by registrant "test".', ], [ 'The required Feature Service "a" has been successfully bound to consumer "b".', ], [ 'The Feature Service "b" has been successfully registered by registrant "test".', ], [ 'The required Feature Service "a" has been successfully bound to consumer "c".', ], [ 'The required Feature Service "b" has been successfully bound to consumer "c".', ], [ 'The Feature Service "c" has been successfully registered by registrant "test".', ], [ 'The required Feature Service "a" has been successfully bound to consumer "foo".', ], [ 'The required Feature Service "b" has been successfully bound to consumer "foo".', ], [ 'The required Feature Service "c" has been successfully bound to consumer "foo".', ], [ 'The required Feature Service "b" has been successfully unbound from consumer "foo".', ], [ 'The required Feature Service "c" has been successfully unbound from consumer "foo".', ], ]); expect(logger.error.mock.calls).toEqual([ [ 'The required Feature Service "a" could not be unbound from consumer "foo".', mockError, ], ]); }); it('fails to unbind an already unbound consumer', () => { const bindings = featureServiceRegistry.bindFeatureServices(providerDefinitionA, providerDefinitionA.id); bindings.unbind(); expect(() => bindings.unbind()).toThrow(new Error('All required Feature Services are already unbound from consumer "a".')); }); it('fails to unbind an already unbound consumer, even if this consumer has been re-bound', () => { const bindings = featureServiceRegistry.bindFeatureServices(providerDefinitionA, providerDefinitionA.id); bindings.unbind(); featureServiceRegistry.bindFeatureServices(providerDefinitionA, providerDefinitionA.id); expect(() => bindings.unbind()).toThrow(new Error('All required Feature Services are already unbound from consumer "a".')); }); }); }); describe('#getInfo', () => { it('returns info about consumers and registered feature services', () => { featureServiceRegistry = new FeatureServiceRegistry({ logger }); providerDefinitionB = { id: 'b', dependencies: { featureServices: { a: '^1.0.0' } }, create: jest.fn(() => ({ '1.0.0': binderB, '2.0.0': binderB })), }; featureServiceRegistry.registerFeatureServices([providerDefinitionA, providerDefinitionB], 'test'); featureServiceRegistry.bindFeatureServices({ dependencies: { featureServices: { a: '1.0.0' } } }, 'foo'); expect(featureServiceRegistry.getInfo()).toEqual({ consumerIds: ['a', 'b', 'foo'], featureServices: [ { id: 'a', versions: ['1.1.0'] }, { id: 'b', versions: ['1.0.0', '2.0.0'] }, ], }); }); }); describe('without a custom logger', () => { let consoleInfoSpy; beforeEach(() => { consoleInfoSpy = jest.spyOn(console, 'info'); featureServiceRegistry = new FeatureServiceRegistry(); }); afterEach(() => { consoleInfoSpy.mockRestore(); }); it('logs messages using the console', () => { featureServiceRegistry.registerFeatureServices([providerDefinitionA], 'test'); expect(consoleInfoSpy.mock.calls).toEqual([ [ 'The Feature Service "a" has been successfully registered by registrant "test".', ], ]); }); }); }); //# sourceMappingURL=feature-service-registry.test.js.map