UNPKG

@rocketsoftware/eureka-js-client

Version:

A JavaScript implementation the Netflix OSS service registry, Eureka.

1,256 lines (1,100 loc) 41 kB
/* eslint-disable no-unused-expressions, max-len */ import sinon from 'sinon'; import chai, { expect } from 'chai'; import sinonChai from 'sinon-chai'; import request from 'request'; import { EventEmitter } from 'events'; import { join } from 'path'; import merge from 'lodash/merge'; import Eureka from '../src/EurekaClient'; import DnsClusterResolver from '../src/DnsClusterResolver'; chai.use(sinonChai); function makeConfig(overrides = {}) { const config = { instance: { app: 'app', vipAddress: '1.2.2.3', hostName: 'myhost', port: 9999, dataCenterInfo: { name: 'MyOwn', }, }, eureka: { host: '127.0.0.1', port: 9999, maxRetries: 0 }, }; return merge({}, config, overrides); } describe('Eureka client', () => { describe('Eureka()', () => { it('should extend EventEmitter', () => { expect(new Eureka(makeConfig())).to.be.instanceof(EventEmitter); }); it('should throw an error if no config is found', () => { function fn() { return new Eureka(); } expect(fn).to.throw(); }); it('should construct with the correct configuration values', () => { function shouldThrow() { return new Eureka(); } function noApp() { return new Eureka({ instance: { vipAddress: true, port: true, dataCenterInfo: { name: 'MyOwn', }, }, eureka: { host: true, port: true, }, }); } function shouldWork() { return new Eureka({ instance: { app: true, vipAddress: true, port: true, dataCenterInfo: { name: 'MyOwn', }, }, eureka: { host: true, port: true, }, }); } function shouldWorkNoInstance() { return new Eureka({ eureka: { registerWithEureka: false, host: true, port: true, }, }); } expect(shouldThrow).to.throw(); expect(noApp).to.throw(/app/); expect(shouldWork).to.not.throw(); expect(shouldWorkNoInstance).to.not.throw(); }); it('should use DnsClusterResolver when configured', () => { const client = new Eureka({ instance: { app: true, vipAddress: true, port: true, dataCenterInfo: { name: 'MyOwn', }, }, eureka: { host: true, port: true, useDns: true, ec2Region: 'my-region', }, }); expect(client.clusterResolver.constructor).to.equal(DnsClusterResolver); }); it('should throw when configured to useDns without setting ec2Region', () => { function shouldThrow() { return new Eureka({ instance: { app: true, vipAddress: true, port: true, dataCenterInfo: { name: 'MyOwn', }, }, eureka: { host: true, port: true, useDns: true, }, }); } expect(shouldThrow).to.throw(/ec2Region/); }); it('should accept requestMiddleware', () => { const requestMiddleware = (opts) => opts; const client = new Eureka({ requestMiddleware, instance: { app: true, vipAddress: true, port: true, dataCenterInfo: { name: 'MyOwn', }, }, eureka: { host: true, port: true, useDns: true, ec2Region: 'my-region', }, }); expect(client.requestMiddleware).to.equal(requestMiddleware); }); }); describe('get instanceId()', () => { it('should return the configured instance id', () => { const instanceId = 'test_id'; const config = makeConfig({ instance: { instanceId, }, }); const client = new Eureka(config); expect(client.instanceId).to.equal(instanceId); }); it('should return hostname for non-AWS datacenters', () => { const config = makeConfig(); const client = new Eureka(config); expect(client.instanceId).to.equal('myhost'); }); it('should return instance ID for AWS datacenters', () => { const config = makeConfig({ instance: { dataCenterInfo: { name: 'Amazon', metadata: { 'instance-id': 'i123' } } }, }); const client = new Eureka(config); expect(client.instanceId).to.equal('i123'); }); }); describe('start()', () => { let config; let client; let registerSpy; let fetchRegistrySpy; let heartbeatsSpy; let registryFetchSpy; beforeEach(() => { config = makeConfig(); client = new Eureka(config); }); afterEach(() => { registerSpy.restore(); fetchRegistrySpy.restore(); heartbeatsSpy.restore(); registryFetchSpy.restore(); }); it('should call register, fetch registry, startHeartbeat and startRegistryFetches', (done) => { registerSpy = sinon.stub(client, 'register').callsArg(0); fetchRegistrySpy = sinon.stub(client, 'fetchRegistry').callsArg(0); heartbeatsSpy = sinon.stub(client, 'startHeartbeats'); registryFetchSpy = sinon.stub(client, 'startRegistryFetches'); const eventSpy = sinon.spy(); client.on('started', eventSpy); client.start(() => { expect(registerSpy).to.have.been.calledOnce; expect(fetchRegistrySpy).to.have.been.calledOnce; expect(heartbeatsSpy).to.have.been.calledOnce; expect(registryFetchSpy).to.have.been.calledOnce; expect(eventSpy).to.have.been.calledOnce; done(); }); }); it('should call fetch registry and startRegistryFetches when registration disabled', (done) => { config = makeConfig({ eureka: { registerWithEureka: false, }, }); client = new Eureka(config); registerSpy = sinon.stub(client, 'register').callsArg(0); fetchRegistrySpy = sinon.stub(client, 'fetchRegistry').callsArg(0); heartbeatsSpy = sinon.stub(client, 'startHeartbeats'); registryFetchSpy = sinon.stub(client, 'startRegistryFetches'); const eventSpy = sinon.spy(); client.on('started', eventSpy); client.start(() => { expect(registerSpy).to.not.have.been.called; expect(fetchRegistrySpy).to.have.been.calledOnce; expect(heartbeatsSpy).to.not.have.been.called; expect(registryFetchSpy).to.have.been.calledOnce; expect(eventSpy).to.have.been.calledOnce; done(); }); }); it('should return error on start failure', (done) => { registerSpy = sinon.stub(client, 'register').yields(new Error('fail')); fetchRegistrySpy = sinon.stub(client, 'fetchRegistry').callsArg(0); heartbeatsSpy = sinon.stub(client, 'startHeartbeats'); registryFetchSpy = sinon.stub(client, 'startRegistryFetches'); const eventSpy = sinon.spy(); client.on('started', eventSpy); client.start((error) => { expect(error).to.match(/fail/); expect(eventSpy).to.not.have.been.called; done(); }); }); }); describe('startHeartbeats()', () => { let config; let client; let renewSpy; let clock; before(() => { config = makeConfig(); client = new Eureka(config); renewSpy = sinon.stub(client, 'renew'); clock = sinon.useFakeTimers(); }); after(() => { renewSpy.restore(); clock.restore(); }); it('should call renew on interval', () => { client.startHeartbeats(); clock.tick(30000); expect(renewSpy).to.have.been.calledOnce; clock.tick(30000); expect(renewSpy).to.have.been.calledTwice; }); }); describe('startRegistryFetches()', () => { let config; let client; let fetchRegistrySpy; let clock; before(() => { config = makeConfig(); client = new Eureka(config); fetchRegistrySpy = sinon.stub(client, 'fetchRegistry'); clock = sinon.useFakeTimers(); }); after(() => { fetchRegistrySpy.restore(); clock.restore(); }); it('should call renew on interval', () => { client.startRegistryFetches(); clock.tick(30000); expect(fetchRegistrySpy).to.have.been.calledOnce; clock.tick(30000); expect(fetchRegistrySpy).to.have.been.calledTwice; }); }); describe('stop()', () => { let config; let client; let deregisterSpy; beforeEach(() => { config = makeConfig(); client = new Eureka(config); deregisterSpy = sinon.stub(client, 'deregister').callsArg(0); }); afterEach(() => { deregisterSpy.restore(); }); it('should call deregister', () => { const stopCb = sinon.spy(); client.stop(stopCb); expect(deregisterSpy).to.have.been.calledOnce; expect(stopCb).to.have.been.calledOnce; }); it('should skip deregister if registration disabled', () => { config = makeConfig({ eureka: { registerWithEureka: false, }, }); client = new Eureka(config); const stopCb = sinon.spy(); client.stop(stopCb); expect(deregisterSpy).to.not.have.been.called; expect(stopCb).to.have.been.calledOnce; }); }); describe('register()', () => { let config; let client; beforeEach(() => { config = makeConfig(); client = new Eureka(config); }); afterEach(() => { request.post.restore(); }); it('should trigger register event', () => { sinon.stub(request, 'post').yields(null, { statusCode: 204 }, null); const eventSpy = sinon.spy(); client.on('registered', eventSpy); client.register(); expect(eventSpy).to.have.been.calledOnce; }); it('should call register URI', () => { sinon.stub(request, 'post').yields(null, { statusCode: 204 }, null); const registerCb = sinon.spy(); client.register(registerCb); expect(request.post).to.have.been.calledWithMatch({ body: { instance: { app: 'app', hostName: 'myhost', dataCenterInfo: { name: 'MyOwn' }, port: 9999, status: 'UP', vipAddress: '1.2.2.3', }, }, json: true, baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/', uri: 'app', }); expect(registerCb).to.have.been.calledWithMatch(null); }); it('should throw error for non-204 response', () => { sinon.stub(request, 'post').yields(null, { statusCode: 500 }, null); const registerCb = sinon.spy(); client.register(registerCb); expect(registerCb).to.have.been.calledWithMatch({ message: 'eureka registration FAILED: status: 500 body: null', }); }); it('should throw error for request error', () => { sinon.stub(request, 'post').yields(new Error('request error'), null, null); const registerCb = sinon.spy(); client.register(registerCb); expect(registerCb).to.have.been.calledWithMatch({ message: 'request error' }); }); }); describe('deregister()', () => { let config; let client; beforeEach(() => { config = makeConfig(); client = new Eureka(config); }); afterEach(() => { request.delete.restore(); }); it('should should trigger deregister event', () => { sinon.stub(request, 'delete').yields(null, { statusCode: 200 }, null); const eventSpy = sinon.spy(); client.on('deregistered', eventSpy); client.register(); client.deregister(); }); it('should call deregister URI', () => { sinon.stub(request, 'delete').yields(null, { statusCode: 200 }, null); const deregisterCb = sinon.spy(); client.deregister(deregisterCb); expect(request.delete).to.have.been.calledWithMatch({ baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/', uri: 'app/myhost', }); expect(deregisterCb).to.have.been.calledWithMatch(null); }); it('should throw error for non-200 response', () => { sinon.stub(request, 'delete').yields(null, { statusCode: 500 }, null); const deregisterCb = sinon.spy(); client.deregister(deregisterCb); expect(deregisterCb).to.have.been.calledWithMatch({ message: 'eureka deregistration FAILED: status: 500 body: null', }); }); it('should throw error for request error', () => { sinon.stub(request, 'delete').yields(new Error('request error'), null, null); const deregisterCb = sinon.spy(); client.deregister(deregisterCb); expect(deregisterCb).to.have.been.calledWithMatch({ message: 'request error' }); }); }); describe('renew()', () => { let config; let client; beforeEach(() => { config = makeConfig(); client = new Eureka(config); }); afterEach(() => { request.put.restore(); }); it('should call heartbeat URI', () => { sinon.stub(request, 'put').yields(null, { statusCode: 200 }, null); client.renew(); expect(request.put).to.have.been.calledWithMatch({ baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/', uri: 'app/myhost', }); }); it('should trigger a heartbeat event', () => { sinon.stub(request, 'put').yields(null, { statusCode: 200 }, null); const eventSpy = sinon.spy(); client.on('heartbeat', eventSpy); client.renew(); expect(eventSpy).to.have.been.calledOnce; }); it('should re-register on 404', () => { sinon.stub(request, 'put').yields(null, { statusCode: 404 }, null); sinon.stub(request, 'post').yields(null, { statusCode: 204 }, null); client.renew(); expect(request.put).to.have.been.calledWithMatch({ baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/', uri: 'app/myhost', }); expect(request.post).to.have.been.calledWithMatch({ body: { instance: { app: 'app', hostName: 'myhost', dataCenterInfo: { name: 'MyOwn' }, port: 9999, status: 'UP', vipAddress: '1.2.2.3', }, }, json: true, baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/', uri: 'app', }); }); }); describe('eureka-client.yml', () => { let stub; before(() => { stub = sinon.stub(process, 'cwd').returns(__dirname); }); after(() => { stub.restore(); }); it('should load the correct', () => { const client = new Eureka(makeConfig()); expect(client.config.eureka.custom).to.equal('test'); }); it('should load the environment overrides', () => { const client = new Eureka(makeConfig()); expect(client.config.eureka.otherCustom).to.equal('test2'); expect(client.config.eureka.overrides).to.equal(2); }); it('should support a `cwd` and `filename` property', () => { const client = new Eureka(makeConfig({ cwd: join(__dirname, 'fixtures'), filename: 'config', })); expect(client.config.eureka.fromFixture).to.equal(true); }); it('should throw error on malformed config file', () => { function malformed() { return new Eureka(makeConfig({ cwd: join(__dirname, 'fixtures'), filename: 'malformed-config', })); } expect(malformed).to.throw(Error); }); it('should not throw error on malformed config file', () => { function missingFile() { return new Eureka(makeConfig({ cwd: join(__dirname, 'fixtures'), filename: 'missing-config', })); } expect(missingFile).to.not.throw(); }); }); describe('validateConfig()', () => { let config; beforeEach(() => { config = makeConfig({ instance: { dataCenterInfo: { name: 'Amazon' } }, }); }); it('should throw an exception with a missing instance.app', () => { function badConfig() { delete config.instance.app; return new Eureka(config); } expect(badConfig).to.throw(TypeError); }); it('should throw an exception with a missing instance.vipAddress', () => { function badConfig() { delete config.instance.vipAddress; return new Eureka(config); } expect(badConfig).to.throw(TypeError); }); it('should throw an exception with a missing instance.port', () => { function badConfig() { delete config.instance.port; return new Eureka(config); } expect(badConfig).to.throw(TypeError); }); it('should throw an exception with a missing instance.dataCenterInfo', () => { function badConfig() { delete config.instance.dataCenterInfo; return new Eureka(config); } expect(badConfig).to.throw(TypeError); }); it('should throw an exception with an invalid request middleware', () => { function badConfig() { config.requestMiddleware = 'invalid middleware'; return new Eureka(config); } expect(badConfig).to.throw(TypeError); }); }); describe('getInstancesByAppId()', () => { let client; let config; beforeEach(() => { config = makeConfig(); client = new Eureka(config); }); it('should throw an exception if no appId is provided', () => { function noAppId() { client.getInstancesByAppId(); } expect(noAppId).to.throw(Error); }); it('should return a list of instances if appId is registered', () => { const appId = 'THESERVICENAME'; const expectedInstances = [{ host: '127.0.0.1' }]; client.cache.app[appId] = expectedInstances; const actualInstances = client.getInstancesByAppId(appId); expect(actualInstances).to.equal(expectedInstances); }); it('should return empty array if no instances were found for given appId', () => { expect(client.getInstancesByAppId('THESERVICENAME')).to.deep.equal([]); }); }); describe('getInstancesByVipAddress()', () => { let client; let config; beforeEach(() => { config = makeConfig(); client = new Eureka(config); }); it('should throw an exception if no vipAddress is provided', () => { function noVipAddress() { client.getInstancesByVipAddress(); } expect(noVipAddress).to.throw(Error); }); it('should return a list of instances if vipAddress is registered', () => { const vipAddress = 'the.vip.address'; const expectedInstances = [{ host: '127.0.0.1' }]; client.cache.vip[vipAddress] = expectedInstances; const actualInstances = client.getInstancesByVipAddress(vipAddress); expect(actualInstances).to.equal(expectedInstances); }); it('should return empty array if no instances were found for given vipAddress', () => { expect(client.getInstancesByVipAddress('the.vip.address')).to.deep.equal([]); }); }); describe('fetchRegistry()', () => { let config; let client; beforeEach(() => { config = makeConfig(); client = new Eureka(config); sinon.stub(client, 'transformRegistry'); sinon.stub(client, 'handleDelta'); }); afterEach(() => { request.get.restore(); client.transformRegistry.restore(); client.handleDelta.restore(); }); it('should should trigger registryUpdated event', () => { sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null); const eventSpy = sinon.spy(); client.on('registryUpdated', eventSpy); client.fetchRegistry(); expect(eventSpy).to.have.been.calledOnce; }); it('should call registry URI', () => { sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null); const registryCb = sinon.spy(); client.fetchRegistry(registryCb); expect(request.get).to.have.been.calledWithMatch({ baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/', uri: '', headers: { Accept: 'application/json' }, }); expect(registryCb).to.have.been.calledWithMatch(null); }); it('should call registry URI for delta', () => { sinon.stub(request, 'get').yields(null, { statusCode: 200 }, '{ "applications": {} }'); const registryCb = sinon.spy(); client.config.shouldUseDelta = true; client.hasFullRegistry = true; client.fetchRegistry(registryCb); expect(request.get).to.have.been.calledWithMatch({ baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/', uri: 'delta', headers: { Accept: 'application/json' }, }); expect(registryCb).to.have.been.calledWithMatch(null); }); it('should throw error for non-200 response', () => { sinon.stub(request, 'get').yields(null, { statusCode: 500 }, null); const registryCb = sinon.spy(); client.fetchRegistry(registryCb); expect(registryCb).to.have.been.calledWithMatch({ message: 'Unable to retrieve full registry from Eureka server', }); }); it('should throw error for non-200 response for delta', () => { sinon.stub(request, 'get').yields(null, { statusCode: 500 }, null); const registryCb = sinon.spy(); client.config.shouldUseDelta = true; client.hasFullRegistry = true; client.fetchRegistry(registryCb); expect(registryCb).to.have.been.calledWithMatch({ message: 'Unable to retrieve delta registry from Eureka server', }); }); it('should throw error for request error', () => { sinon.stub(request, 'get').yields(new Error('request error'), null, null); const registryCb = sinon.spy(); client.fetchRegistry(registryCb); expect(registryCb).to.have.been.calledWithMatch({ message: 'request error' }); }); it('should throw error for request error for delta request', () => { sinon.stub(request, 'get').yields(new Error('request error'), null, null); const registryCb = sinon.spy(); client.config.shouldUseDelta = true; client.hasFullRegistry = true; client.fetchRegistry(registryCb); expect(registryCb).to.have.been.calledWithMatch({ message: 'request error' }); }); it('should throw error on invalid JSON', () => { sinon.stub(request, 'get').yields(null, { statusCode: 200 }, '{ blah'); const registryCb = sinon.spy(); client.fetchRegistry(registryCb); expect(registryCb).to.have.been.calledWith(new SyntaxError()); }); it('should throw error on invalid JSON for delta request', () => { sinon.stub(request, 'get').yields(null, { statusCode: 200 }, '{ blah'); const registryCb = sinon.spy(); client.config.shouldUseDelta = true; client.hasFullRegistry = true; client.fetchRegistry(registryCb); expect(registryCb).to.have.been.calledWith(new SyntaxError()); }); }); describe('transformRegistry()', () => { let client; let config; let registry; let instance1; let instance2; let instance3; let instance4; let instance5; let app1; let app2; let app3; beforeEach(() => { config = makeConfig(); registry = { applications: { application: {} }, }; instance1 = { hostName: '127.0.0.1', port: { $: 1000 }, app: 'theapp', vipAddress: 'vip1', status: 'UP' }; instance2 = { hostName: '127.0.0.2', port: { $: 2000 }, app: 'theapptwo', vipAddress: 'vip2', status: 'UP' }; instance3 = { hostName: '127.0.0.3', port: { $: 2000 }, app: 'theapp', vipAddress: 'vip2', status: 'UP' }; instance4 = { hostName: '127.0.0.4', port: { $: 2000 }, app: 'theappthree', vipAddress: 'vip3', status: 'UP' }; instance5 = { hostName: '127.0.0.5', port: { $: 2000 }, app: 'theappthree', vipAddress: 'vip2', status: 'UP' }; app1 = { name: 'theapp', instance: instance1 }; app2 = { name: 'theapptwo', instance: [instance2, instance3] }; app3 = { name: 'theappthree', instance: [instance5, instance4] }; client = new Eureka(config); }); it('should noop if empty registry', () => { client.transformRegistry(undefined); expect(client.cache.vip).to.be.empty; expect(client.cache.app).to.be.empty; }); it('should return clear the cache if no applications exist', () => { registry.applications.application = null; client.transformRegistry(registry); expect(client.cache.vip).to.be.empty; expect(client.cache.app).to.be.empty; }); it('should transform a registry with one app', () => { registry.applications.application = app1; client.transformRegistry(registry); expect(client.cache.app[app1.name.toUpperCase()].length).to.equal(1); expect(client.cache.vip[instance1.vipAddress].length).to.equal(1); }); it('should transform a registry with two or more apps', () => { registry.applications.application = [app1, app2]; client.transformRegistry(registry); expect(client.cache.app[app1.name.toUpperCase()].length).to.equal(2); expect(client.cache.vip[instance2.vipAddress].length).to.equal(2); }); it('should transform a registry with a single application with multiple vips', () => { registry.applications.application = [app3]; client.transformRegistry(registry); expect(client.cache.app[app3.name.toUpperCase()].length).to.equal(2); expect(client.cache.vip[instance5.vipAddress].length).to.equal(1); expect(client.cache.vip[instance4.vipAddress].length).to.equal(1); }); }); describe('transformApp()', () => { let client; let config; let app; let instance1; let instance2; let instance3; let instance4; let downInstance; let theVip; let multiVip; let cache; beforeEach(() => { config = makeConfig({ instance: { dataCenterInfo: { name: 'Amazon' } }, }); client = new Eureka(config); theVip = 'theVip'; multiVip = 'fooVip,barVip'; instance1 = { hostName: '127.0.0.1', port: 1000, vipAddress: theVip, app: 'theapp', status: 'UP' }; instance2 = { hostName: '127.0.0.2', port: 2000, vipAddress: theVip, app: 'theapp', status: 'UP' }; instance3 = { hostName: '127.0.0.5', port: 2000, vipAddress: multiVip, app: 'theapp', status: 'UP' }; instance4 = { hostName: '127.0.0.6', port: 2000, vipAddress: void 0, app: 'theapp', status: 'UP' }; downInstance = { hostName: '127.0.0.7', port: 2000, app: 'theapp', vipAddress: theVip, status: 'DOWN' }; app = { name: 'theapp' }; cache = { app: {}, vip: {} }; }); it('should transform an app with one instance', () => { app.instance = instance1; client.transformApp(app, cache); expect(cache.app[app.name.toUpperCase()].length).to.equal(1); expect(cache.vip[theVip].length).to.equal(1); }); it('should transform an app with one instance that has a comma separated vipAddress', () => { app.instance = instance3; client.transformApp(app, cache); expect(cache.app[app.name.toUpperCase()].length).to.equal(1); expect(cache.vip[multiVip.split(',')[0]].length).to.equal(1); expect(cache.vip[multiVip.split(',')[1]].length).to.equal(1); }); it('should transform an app with one instance that has no vipAddress', () => { app.instance = instance4; client.transformApp(app, cache); expect(cache.app[app.name.toUpperCase()].length).to.equal(1); expect(Object.keys(cache.vip).length).to.equal(0); }); it('should transform an app with two or more instances', () => { app.instance = [instance1, instance2, instance3]; client.transformApp(app, cache); expect(cache.app[app.name.toUpperCase()].length).to.equal(3); expect(cache.vip[theVip].length).to.equal(2); expect(cache.vip[multiVip.split(',')[0]].length).to.equal(1); expect(cache.vip[multiVip.split(',')[1]].length).to.equal(1); }); it('should filter UP instances by default', () => { app.instance = [instance1, instance2, downInstance]; client.transformApp(app, cache); expect(cache.app[app.name.toUpperCase()].length).to.equal(2); expect(cache.vip[theVip].length).to.equal(2); }); it('should not filter UP instances when filterUpInstances === false', () => { config = makeConfig({ instance: { dataCenterInfo: { name: 'Amazon' } }, eureka: { filterUpInstances: false }, }); client = new Eureka(config); app.instance = [instance1, instance2, downInstance]; client.transformApp(app, cache); expect(cache.app[app.name.toUpperCase()].length).to.equal(3); expect(cache.vip[theVip].length).to.equal(3); }); }); describe('addInstanceMetadata()', () => { let client; let config; let instanceConfig; let awsMetadata; let metadataSpy; beforeEach(() => { instanceConfig = { app: 'app', vipAddress: '1.2.3.4', port: 9999, dataCenterInfo: { name: 'Amazon' }, statusPageUrl: 'http://__HOST__:8080/info', healthCheckUrl: 'http://__HOST__:8077/healthcheck', homePageUrl: 'http://__HOST__:8080/', }; awsMetadata = { 'public-hostname': 'ec2-127-0-0-1.us-fake-1.mydomain.com', 'public-ipv4': '54.54.54.54', 'local-hostname': 'fake-1', 'local-ipv4': '10.0.1.1', }; }); afterEach(() => { client.metadataClient.fetchMetadata.restore(); }); it('should update hosts with AWS metadata public host', () => { // Setup config = { instance: instanceConfig, eureka: { host: '127.0.0.1', port: 9999 }, }; client = new Eureka(config); metadataSpy = sinon.spy(); sinon.stub(client.metadataClient, 'fetchMetadata').yields(awsMetadata); // Act client.addInstanceMetadata(metadataSpy); expect(client.config.instance.hostName).to.equal('ec2-127-0-0-1.us-fake-1.mydomain.com'); expect(client.config.instance.ipAddr).to.equal('54.54.54.54'); expect(client.config.instance.statusPageUrl).to.equal('http://ec2-127-0-0-1.us-fake-1.mydomain.com:8080/info'); expect(client.config.instance.healthCheckUrl).to.equal('http://ec2-127-0-0-1.us-fake-1.mydomain.com:8077/healthcheck'); expect(client.config.instance.homePageUrl).to.equal('http://ec2-127-0-0-1.us-fake-1.mydomain.com:8080/'); }); it('should update hosts with AWS metadata public IP when preferIpAddress === true', () => { // Setup config = { instance: instanceConfig, eureka: { host: '127.0.0.1', port: 9999, preferIpAddress: true }, }; client = new Eureka(config); metadataSpy = sinon.spy(); sinon.stub(client.metadataClient, 'fetchMetadata').yields(awsMetadata); // Act client.addInstanceMetadata(metadataSpy); expect(client.config.instance.hostName).to.equal('54.54.54.54'); expect(client.config.instance.ipAddr).to.equal('54.54.54.54'); expect(client.config.instance.statusPageUrl).to.equal('http://54.54.54.54:8080/info'); expect(client.config.instance.healthCheckUrl).to.equal('http://54.54.54.54:8077/healthcheck'); expect(client.config.instance.homePageUrl).to.equal('http://54.54.54.54:8080/'); }); it('should update hosts with AWS metadata local host if useLocalMetadata === true', () => { // Setup config = { instance: instanceConfig, eureka: { host: '127.0.0.1', port: 9999, useLocalMetadata: true }, }; client = new Eureka(config); metadataSpy = sinon.spy(); sinon.stub(client.metadataClient, 'fetchMetadata').yields(awsMetadata); // Act client.addInstanceMetadata(metadataSpy); expect(client.config.instance.hostName).to.equal('fake-1'); expect(client.config.instance.ipAddr).to.equal('10.0.1.1'); expect(client.config.instance.statusPageUrl).to.equal('http://fake-1:8080/info'); expect(client.config.instance.healthCheckUrl).to.equal('http://fake-1:8077/healthcheck'); expect(client.config.instance.homePageUrl).to.equal('http://fake-1:8080/'); }); it('should update hosts with AWS metadata local IP if useLocalMetadata === true' + ' and preferIpAddress === true', () => { // Setup config = { instance: instanceConfig, eureka: { host: '127.0.0.1', port: 9999, useLocalMetadata: true, preferIpAddress: true }, }; client = new Eureka(config); metadataSpy = sinon.spy(); sinon.stub(client.metadataClient, 'fetchMetadata').yields(awsMetadata); // Act client.addInstanceMetadata(metadataSpy); expect(client.config.instance.hostName).to.equal('10.0.1.1'); expect(client.config.instance.ipAddr).to.equal('10.0.1.1'); expect(client.config.instance.statusPageUrl).to.equal('http://10.0.1.1:8080/info'); expect(client.config.instance.healthCheckUrl).to.equal('http://10.0.1.1:8077/healthcheck'); expect(client.config.instance.homePageUrl).to.equal('http://10.0.1.1:8080/'); }); }); describe('eurekaRequest()', () => { beforeEach(() => {}); afterEach(() => { if (request.get.restore) request.get.restore(); }); it('should call requestMiddleware with request options', () => { const overrides = { requestMiddleware: sinon.spy((opts, done) => done(opts)), }; const config = makeConfig(overrides); const client = new Eureka(config); sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null); client.eurekaRequest({}, (error) => { expect(Boolean(error)).to.equal(false); expect(overrides.requestMiddleware).to.be.calledOnce; expect(overrides.requestMiddleware.args[0][0]).to.be.an('object'); }); }); it('should catch an error in requestMiddleware', () => { const overrides = { requestMiddleware: sinon.spy((opts, done) => { done(); }), }; const config = makeConfig(overrides); const client = new Eureka(config); sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null); client.eurekaRequest({}, (error) => { expect(overrides.requestMiddleware).to.be.calledOnce; expect(error).to.be.an('error'); }); }); it('should check the returnType of requestMiddleware', () => { const overrides = { requestMiddleware: sinon.spy((opts, done) => done('foo')), }; const config = makeConfig(overrides); const client = new Eureka(config); sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null); client.eurekaRequest({}, (error) => { expect(error).to.be.an('error'); expect(error.message).to.equal('requestMiddleware did not return an object'); }); }); it('should retry next server on request failure', (done) => { const overrides = { eureka: { serviceUrls: { default: ['http://serverA', 'http://serverB'], }, maxRetries: 3, requestRetryDelay: 0, }, }; const config = makeConfig(overrides); const client = new Eureka(config); const requestStub = sinon.stub(request, 'get'); requestStub.onCall(0).yields(null, { statusCode: 500 }, null); requestStub.onCall(1).yields(null, { statusCode: 200 }, null); client.eurekaRequest({ uri: '/path' }, (error) => { expect(error).to.be.null; expect(requestStub).to.be.calledTwice; expect(requestStub.args[0][0]).to.have.property('baseUrl', 'http://serverA'); expect(requestStub.args[1][0]).to.have.property('baseUrl', 'http://serverB'); done(); }); }); }); describe('handleDelta()', () => { let client; beforeEach(() => { const config = makeConfig({ shouldUseDelta: true }); client = new Eureka(config); }); it('should add instances', () => { const appDelta = [ { instance: [ { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'ADDED' }, ], }, ]; client.handleDelta(client.cache, appDelta); expect(client.cache.vip.thevip).to.have.length(1); expect(client.cache.app.THEAPP).to.have.length(1); }); it('should handle duplicate instances on add', () => { const appDelta = [ { instance: [ { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'ADDED' }, { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'ADDED' }, ], }, ]; client.handleDelta(client.cache, appDelta); expect(client.cache.vip.thevip).to.have.length(1); expect(client.cache.app.THEAPP).to.have.length(1); }); it('should modify instances', () => { const appDelta = [ { instance: [ { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'MODIFIED', newProp: 'foo' }, ], }, ]; const original = { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'MODIFIED' }; client.cache = { app: { THEAPP: [original] }, vip: { thevip: [original] }, }; client.handleDelta(client.cache, appDelta); expect(client.cache.vip.thevip).to.have.length(1); expect(client.cache.app.THEAPP).to.have.length(1); expect(client.cache.vip.thevip[0]).to.have.property('newProp'); expect(client.cache.app.THEAPP[0]).to.have.property('newProp'); }); it('should modify instances even when status is not UP', () => { const appDelta = [ { instance: [ { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'DOWN', actionType: 'MODIFIED', newProp: 'foo' }, ], }, ]; const original = { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'MODIFIED' }; client.cache = { app: { THEAPP: [original] }, vip: { thevip: [original] }, }; client.handleDelta(client.cache, appDelta); expect(client.cache.vip.thevip).to.have.length(1); expect(client.cache.app.THEAPP).to.have.length(1); expect(client.cache.vip.thevip[0]).to.have.property('newProp'); expect(client.cache.app.THEAPP[0]).to.have.property('newProp'); }); it('should add if instance doesnt exist when modifying', () => { const appDelta = [ { instance: [ { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'MODIFIED', newProp: 'foo' }, ], }, ]; client.handleDelta(client.cache, appDelta); expect(client.cache.vip.thevip).to.have.length(1); expect(client.cache.app.THEAPP).to.have.length(1); expect(client.cache.vip.thevip[0]).to.have.property('newProp'); expect(client.cache.app.THEAPP[0]).to.have.property('newProp'); }); it('should delete instances', () => { const appDelta = [ { instance: [ { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'DELETED', newProp: 'foo' }, ], }, ]; const original = { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'ADDED' }; client.cache = { app: { THEAPP: [original] }, vip: { thevip: [original] }, }; client.handleDelta(client.cache, appDelta); expect(client.cache.vip.thevip).to.have.length(0); expect(client.cache.app.THEAPP).to.have.length(0); }); it('should not delete instances if they do not exist', () => { const appDelta = [ { instance: [ { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'DELETED', newProp: 'foo' }, ], }, ]; client.cache = { app: { THEAPP: [] }, vip: { thevip: [] }, }; client.handleDelta(client.cache, appDelta); expect(client.cache.vip.thevip).to.have.length(0); expect(client.cache.app.THEAPP).to.have.length(0); }); }); });