UNPKG

jsonmerger

Version:

A merger for separate JSON (config) files supporting merge strategies and preprocessing

849 lines (802 loc) 24.1 kB
'use strict'; describe('jsonMerger', () => { const chai = require('chai'); const sinonChai = require('sinon-chai'); chai.use(sinonChai); const expect = chai.expect; const sinon = require('sinon'); const proxyquire = require('proxyquire').noCallThru(); let os; let jsonMerger; let result; let dc; let ic; let ec; let dcin; let icin; let ecin; function defaultConfig() { return { foo: 'default global foo', something: 'default global something', instances: { _instances: 'true', '*': { x: [0], a: 'default * a', b: 'default * b', c: 'default * c', d: 'default * d', deeper: { zero: 0 } }, unittest: { x: [1], b: 'default unittest b', c: 'default unittest c', d: 'default unittest d', deeper: { one: 1 } } } }; } function instancesConfig() { return { something: 'instances global something', instances: { '*': { x: [2], c: 'instances * c', d: 'instances * d', deeper: { two: 2 } }, unittest: { x: [3], d: 'instances unittest d', deeper: { three: 3 } } } }; } function environmentsConfig() { return { _preprocess: 'stages', prod: { hosts: ['prodhost'], env: 'prodenv', config: { env: 'prod', something: 'environments prod global something' } }, dev: { hosts: ['devhost'], env: 'devenv', config: { instances: { '*': { x: [4], a: 'environments dev instances * a', deeper: { four: 4 } } } } } }; } beforeEach(() => { result = undefined; os = { hostname: sinon.stub().returns('prodhost') }; // Input references dcin = defaultConfig(); icin = instancesConfig(); ecin = environmentsConfig(); jsonMerger = proxyquire('../lib/jsonmerger', { os, defaults: dcin, instances: icin, environments: ecin }); // For easy result comparisons: dc = defaultConfig(); ic = instancesConfig(); ec = environmentsConfig(); }); describe('with a single config file', () => { const files = ['defaults']; it('should retain all global properties', () => { result = jsonMerger(files); expect(result.foo).to.deep.equal(dc.foo); expect(result.something).to.deep.equal(dc.something); }); it('should drop the `*` instance from the config', () => { result = jsonMerger(files); expect(Object.keys(result.instances)).to.have.length(1); expect(result.instances).to.not.have.key('*'); }); it('should properly merge each instance with *', () => { result = jsonMerger(files); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: dc.instances.unittest.c, d: dc.instances.unittest.d, x: [0, 1], deeper: { zero: 0, one: 1 } }); }); it('should add a getter and setter method to the result object', () => { result = jsonMerger(files); expect(result.get).to.be.a.function; expect(result.set).to.be.a.function; }); it('should properly merge arrays from nested wildcards', () => { icin = { level1: { _instances: true, '*': { level2: { _instances: true, '*': { array: [ 'one', ] }, specific2: { _strategy: 'noconcat', array: [ 'two', ] } } }, specific1: { level2: { specific2: { array: [ 'three' ] } } } } }; jsonMerger = proxyquire('../lib/jsonmerger', { os, defaults: dcin, instances: icin, environments: ecin }); result = jsonMerger(['instances']); expect(result.level1.specific1.level2.specific2.array).to.deep.equal(['two', 'three']); }); }); describe('with two (or more) regular config files', () => { const files = ['defaults', 'instances']; it('should merge global variables in order', () => { result = jsonMerger(files); expect(result.foo).to.deep.equal(dc.foo); expect(result.something).to.deep.equal(ic.something); }); it('should properly merge the * instances (1)', () => { dcin.instances.tmptest = {}; result = jsonMerger(files); expect(result.instances.tmptest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances['*'].b, c: ic.instances['*'].c, d: ic.instances['*'].d, x: [0, 2], deeper: { zero: 0, two: 2 } }); }); it('should properly merge the * instances (2)', () => { icin.instances.tmptest = {}; result = jsonMerger(files); expect(result.instances.tmptest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances['*'].b, c: ic.instances['*'].c, d: ic.instances['*'].d, x: [0, 2], deeper: { zero: 0, two: 2 } }); }); it('should properly merge nested * instances', () => { dcin.instances['*'].subinstances = { _instances: true, '*': { one: 1, two: 2, three: 3 }, instance: { three: 33, four: 4 }, emptyDefault: {} }; icin.instances['*'].subinstances = { '*': { two: 22 }, instance: { four: 44 }, newinstance: { one: 11, five: 5 }, emptyInstance: {} }; icin.instances.tmptest = {}; result = jsonMerger(files); expect(result.instances.tmptest.subinstances).to.deep.equal({ emptyDefault: { one: 1, two: 22, three: 3 }, emptyInstance: { one: 1, two: 22, three: 3 }, instance: { one: 1, two: 22, three: 33, four: 44 }, newinstance: { one: 11, two: 22, three: 3, five: 5 } }); }); it('should properly merge arrays from nested wildcards when instances are configured in the default', () => { dcin = { level1: { _instances: true, '*': { level2: { _instances: true } } } }; icin = { level1: { '*': { level2: { '*': { array: [ 'one', ] }, specific2: { _strategy: 'noconcat', array: [ 'two', ] } } }, specific1: { level2: { specific2: {} } } } }; jsonMerger = proxyquire('../lib/jsonmerger', { os, defaults: dcin, instances: icin, environments: ecin }); result = jsonMerger(files); expect(result.level1.specific1.level2.specific2.array).to.deep.equal(['two']); }); it('should drop the `*` instance from the config', () => { result = jsonMerger(files); expect(Object.keys(result.instances)).to.have.length(1); expect(result.instances).to.not.have.key('*'); }); it('should merge project instances in correct order', () => { result = jsonMerger(files); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [0, 1, 2, 3], deeper: { zero: 0, one: 1, two: 2, three: 3 } }); }); it('should add a getter and setter method to the result object', () => { result = jsonMerger(files); expect(result.get).to.be.a.function; expect(result.set).to.be.a.function; }); }); describe('with an environments config file', () => { const files = ['defaults', 'instances', 'environments']; it('should remove the preprocess directive when done', () => { result = jsonMerger(files); expect(ecin).to.not.have.keys('_preprocess'); expect(result).to.not.have.keys('_preprocess'); }); it('should properly merge the environments file based on host (1)', () => { result = jsonMerger(files); delete result.get; delete result.set; expect(result).to.deep.equal({ env: ec.prod.config.env, foo: dc.foo, something: ec.prod.config.something, instances: { unittest: { a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [0, 1, 2, 3], deeper: { zero: 0, one: 1, two: 2, three: 3 } } }, }); }); it('should properly merge the environments file based on host (2)', () => { os.hostname = sinon.stub().returns('devhost'); result = jsonMerger(files); delete result.get; delete result.set; expect(result).to.deep.equal({ env: ec.prod.config.env, foo: dc.foo, something: ec.prod.config.something, instances: { unittest: { a: ec.dev.config.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [0, 1, 2, 3, 4], deeper: { zero: 0, one: 1, two: 2, three: 3, four: 4 } } } }); }); it('should properly merge the environments file based on env variable (1)', () => { process.env.NODE_ENV = 'prodenv'; os.hostname = sinon.stub().returns('localhost'); result = jsonMerger(files); delete result.get; delete result.set; expect(result).to.deep.equal({ env: ec.prod.config.env, foo: dc.foo, something: ec.prod.config.something, instances: { unittest: { a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [0, 1, 2, 3], deeper: { zero: 0, one: 1, two: 2, three: 3 } } }, }); }); it('should properly merge the environments file based on env variable (2)', () => { process.env.NODE_ENV = 'devenv'; os.hostname = sinon.stub().returns('localhost'); result = jsonMerger(files); delete result.get; delete result.set; expect(result).to.deep.equal({ env: ec.prod.config.env, foo: dc.foo, something: ec.prod.config.something, instances: { unittest: { a: ec.dev.config.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [0, 1, 2, 3, 4], deeper: { zero: 0, one: 1, two: 2, three: 3, four: 4 } } } }); }); it('should drop the `*` instance from the config', () => { result = jsonMerger(files); expect(Object.keys(result.instances)).to.have.length(1); expect(result.instances).to.not.have.key('*'); }); it('should add a getter and setter method to the result object', () => { result = jsonMerger(files); expect(result.get).to.be.a.function; expect(result.set).to.be.a.function; }); }); describe('using alternative merge strategy', () => { describe('noconcat', () => { it('should properly merge each instance with *', () => { dcin.instances.unittest._strategy = 'noconcat'; result = jsonMerger(['defaults']); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: dc.instances.unittest.c, d: dc.instances.unittest.d, x: [1], deeper: { zero: 0, one: 1 } }); }); it('should properly merge the * instances', () => { dcin.instances.tmptest = {}; icin.instances['*']._strategy = 'noconcat'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.tmptest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances['*'].b, c: ic.instances['*'].c, d: ic.instances['*'].d, x: [2], deeper: { zero: 0, two: 2 } }); }); it('should merge project instances in correct order (1)', () => { dcin.instances.unittest._strategy = 'noconcat'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [1, 2, 3], // 0 was replaced by 1, the rest was properly merged deeper: { zero: 0, one: 1, two: 2, three: 3 } }); }); it('should merge project instances in correct order (2)', () => { icin.instances['*']._strategy = 'noconcat'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [2, 3], // [0,1] was replaced by 2, 3 was concatenated again afterwards deeper: { zero: 0, one: 1, two: 2, three: 3 } }); }); it('should merge project instances in correct order (3)', () => { icin.instances.unittest._strategy = 'noconcat'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [3], // Full replace deeper: { zero: 0, one: 1, two: 2, three: 3 } }); }); it('should properly merge objects from the highest level', () => { dcin.y = [0]; icin.y = [1]; icin._strategy = 'noconcat'; result = jsonMerger(['defaults', 'instances']); expect(result.y).to.deep.equal([1]); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [0, 1, 2, 3], deeper: { zero: 0, one: 1, two: 2, three: 3, } }); }); it('should merge deeply nested project instances in correct order', () => { dcin.instances.unittest.deeper.y = [0]; icin.instances.unittest.deeper.y = [1]; icin.instances.unittest.deeper._strategy = 'noconcat'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [0, 1, 2, 3], // Full replace deeper: { y: [1], zero: 0, one: 1, two: 2, three: 3 } }); }); it('should properly handle the noconcat strategy on environments', () => { const files = ['defaults', 'instances', 'environments']; dcin.testproperty = ['foo']; icin.testproperty = ['bar']; ecin.prod.config = { _strategy: 'noconcat', testproperty: ['lorem'] }; result = jsonMerger(files); delete result.get; delete result.set; expect(result.testproperty).to.deep.equal(['lorem']); }); }); describe('replace', () => { it('should properly merge each instance with *', () => { dcin.instances.unittest._strategy = 'replace'; result = jsonMerger(['defaults']); expect(result.instances.unittest).to.deep.equal({ b: dc.instances.unittest.b, c: dc.instances.unittest.c, d: dc.instances.unittest.d, x: [1], deeper: { one: 1 } }); }); it('should properly merge the * instances', () => { dcin.instances.tmptest = {}; icin.instances['*']._strategy = 'replace'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.tmptest).to.deep.equal({ c: ic.instances['*'].c, d: ic.instances['*'].d, x: [2], deeper: { two: 2 } }); }); it('should merge project instances in correct order (1)', () => { dcin.instances.unittest._strategy = 'replace'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.deep.equal({ b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [1, 2, 3], // 0 was replaced by 1, the rest was properly merged deeper: { one: 1, two: 2, three: 3 } }); }); it('should merge project instances in correct order (2)', () => { icin.instances.unittest._strategy = 'replace'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.deep.equal({ x: [3], d: 'instances unittest d', deeper: { three: 3 } }); }); it('should properly merge objects from the highest level', () => { dcin.someObject = { foo: 'foo' }; icin.someObject = { bar: 'bar' }; icin._strategy = 'replace'; // It's important we define the new `_instances` directive again since // the "old" one will never be read by the `replace` strategy. icin.instances._instances = true; result = jsonMerger(['defaults', 'instances']); expect(result.someObject).to.deep.equal({ bar: 'bar' }); expect(result.instances.unittest).to.deep.equal({ c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [2, 3], deeper: { two: 2, three: 3 } }); }); it('should merge deeply nested project instances in correct order', () => { icin.instances.unittest.deeper._strategy = 'replace'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.deep.equal({ a: dc.instances['*'].a, b: dc.instances.unittest.b, c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [0, 1, 2, 3], deeper: { three: 3 } }); }); it('should properly handle the replace strategy on environments', () => { const files = ['defaults', 'instances', 'environments']; ecin.prod.config._strategy = 'replace'; result = jsonMerger(files); delete result.get; delete result.set; expect(result).to.deep.equal({ env: 'prod', something: 'environments prod global something' }); }); it('should properly handle the replace strategy on environment properties', () => { const files = ['defaults', 'instances', 'environments']; icin.testproperty = { foo: 'bar' }; ecin.prod.config.testproperty = { _strategy: 'replace', lorem: 'ipsum' }; result = jsonMerger(files); delete result.get; delete result.set; expect(result.testproperty).to.deep.equal({ lorem: 'ipsum' }); }); }); describe('delete', () => { it('should properly merge each instance with *', () => { dcin.instances.unittest._strategy = 'delete'; result = jsonMerger(['defaults']); expect(result.instances.unittest).to.be.undefined; }); it('should properly merge the * instances', () => { dcin.instances.tmptest = {}; icin.instances['*']._strategy = 'delete'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.tmptest).to.be.undefined; }); it('should merge project instances in correct order (1)', () => { dcin.instances.unittest._strategy = 'delete'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.deep.equal({ c: ic.instances['*'].c, d: ic.instances.unittest.d, x: [2, 3], // 0 and 1 were deleted, the rest was properly merged deeper: { two: 2, three: 3 } }); }); it('should merge project instances in correct order (2)', () => { icin.instances.unittest._strategy = 'delete'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest).to.be.undefined; }); it('should properly merge objects from the highest level', () => { icin._strategy = 'delete'; result = jsonMerger(['defaults', 'instances']); expect(result).to.be.undefined; }); it('should merge deeply nested project instances in correct order', () => { icin.instances.unittest.deeper._strategy = 'delete'; result = jsonMerger(['defaults', 'instances']); expect(result.instances.unittest.deeper).to.be.undefined; }); it('should not delete null values when configured', () => { dcin.instances.tmptest = {}; icin.instances['*']._strategy = 'delete'; result = jsonMerger(['defaults', 'instances'], { deleteNullValues: false }); expect(result.instances.tmptest).to.be.null; }); it('should properly handle the delete strategy on environments', () => { const files = ['defaults', 'instances', 'environments']; ecin.prod.config._strategy = 'delete'; result = jsonMerger(files); expect(result).to.be.undefined; }); it('should properly handle the delete strategy on environment properties', () => { const files = ['defaults', 'instances', 'environments']; icin.testproperty = { foo: 'bar' }; ecin.prod.config.testproperty = { _strategy: 'delete' }; result = jsonMerger(files); delete result.get; delete result.set; expect(result).not.to.have.property('testproperty'); }); }); }); });