UNPKG

@deepstream/client

Version:
622 lines (514 loc) 18.4 kB
import { expect } from 'chai' import * as sinon from 'sinon' import { getServicesMock, getLastMessageSent } from '../test/mocks' import { EVENT, TOPIC, RPC_ACTION, Message, RPCMessage } from '../constants' import { DefaultOptions } from '../client-options' import { RPCHandler, RPCProvider } from './rpc-handler' import { RPCResponse } from './rpc-response' import { TimeoutRegistry } from '../util/timeout-registry' import { PromiseDelay } from '../util/utils' describe('RPC handler', () => { let services: any let rpcHandler: RPCHandler let handle: Function let rpcProviderSpy: sinon.SinonSpy let rpcMakeSpy: sinon.SinonSpy let data: any const name = 'myRpc' const rpcAcceptTimeout = 10 const rpcResponseTimeout = 30 const options = { ...DefaultOptions, rpcAcceptTimeout, rpcResponseTimeout, subscriptionInterval: 0 } beforeEach(() => { services = getServicesMock() rpcHandler = new RPCHandler(services, options) handle = services.getHandle() rpcProviderSpy = sinon.spy() rpcMakeSpy = sinon.spy() data = { foo: 'bar' } }) afterEach(() => { services.connectionMock.verify() services.timeoutRegistryMock.verify() services.loggerMock.verify() }) it('validates parameters on provide, unprovide and make', () => { expect(rpcHandler.provide.bind(rpcHandler, '', () => {})).to.throw() expect(rpcHandler.unprovide.bind(rpcHandler, '')).to.throw() expect(rpcHandler.unprovide.bind(rpcHandler)).to.throw() expect(rpcHandler.make.bind(rpcHandler, '')).to.throw() expect(rpcHandler.make.bind(rpcHandler)).to.throw() }) it('registers a provider', () => { const message = { topic: TOPIC.RPC, action: RPC_ACTION.PROVIDE, names: [name], correlationId: '0' } services.connectionMock .expects('sendMessage') .once() .withExactArgs(message) services.timeoutRegistryMock .expects('add') .once() .withExactArgs({ message }) rpcHandler.provide(name, rpcProviderSpy as RPCProvider) sinon.assert.notCalled(rpcProviderSpy) }) it('gets provider names', () => { rpcHandler.provide('rpc1', rpcProviderSpy as RPCProvider) rpcHandler.provide('rpc2', rpcProviderSpy as RPCProvider) expect(rpcHandler.providerNames()).to.deep.equal(['rpc1', 'rpc2']) rpcHandler.unprovide('rpc2') expect(rpcHandler.providerNames()).to.deep.equal(['rpc1']) }) it('reregisters a provider after a connection reconnection', () => { const message = { topic: TOPIC.RPC, action: RPC_ACTION.PROVIDE, names: [name], correlationId: '0' } services.connectionMock .expects('sendMessage') .twice() .withExactArgs(message) services.timeoutRegistryMock .expects('add') .twice() .withExactArgs({ message }) rpcHandler.provide(name, rpcProviderSpy as RPCProvider) services.simulateConnectionLost() services.simulateConnectionReestablished() sinon.assert.notCalled(rpcProviderSpy) }) it('sends rpc request message on make', () => { services.connectionMock .expects('sendMessage') .once() .withExactArgs({ topic: TOPIC.RPC, action: RPC_ACTION.REQUEST, name, parsedData: data, correlationId: sinon.match.any }) rpcHandler.make(name, data, () => {}) }) it('returns promise on make when no callback is passed', () => { services.connectionMock .expects('sendMessage') .once() const promise = rpcHandler.make(name, data) expect(promise).to.be.a('promise') }) it('cant\'t make requests when client is offline', async () => { const callback = sinon.spy() const promisseError = sinon.spy() const promisseSuccess = sinon.spy() services.connection.isConnected = false rpcHandler.make(name, data, callback) const promise = rpcHandler.make(name, data) promise.then(promisseSuccess).catch(promisseError) await PromiseDelay(1) sinon.assert.calledOnce(callback) sinon.assert.calledWithExactly(callback, EVENT.CLIENT_OFFLINE) sinon.assert.notCalled(promisseSuccess) sinon.assert.calledOnce(promisseError) sinon.assert.calledWithExactly(promisseError, EVENT.CLIENT_OFFLINE) }) it('doesn\'t reply rpc and sends rejection if no provider exists', () => { services.connectionMock .expects('sendMessage') .once() .withExactArgs({ topic: TOPIC.RPC, action: RPC_ACTION.REJECT, name, correlationId: '123' }) services.timeoutRegistryMock .expects('add') .never() handle({ topic: TOPIC.RPC, action: RPC_ACTION.REQUEST, name, parsedData: data, correlationId: '123' }) }) it('handles ack messages', () => { const message = { topic: TOPIC.RPC, action: RPC_ACTION.PROVIDE, name, isAck: true } services.timeoutRegistryMock .expects('remove') .once() .withExactArgs(message) handle(message) }) it('handles permission and message denied errors for provide and unprovide', () => { const expectations = (message: Message) => { services.timeoutRegistryMock .expects('remove') .once() .withExactArgs(message) services.loggerMock .expects('error') .once() .withExactArgs(message) } const permissionErrProvidingMsg = { topic: TOPIC.RPC, action: RPC_ACTION.MESSAGE_PERMISSION_ERROR, name, originalAction: RPC_ACTION.PROVIDE } const permissionErrUnprovidingMsg = { topic: TOPIC.RPC, action: RPC_ACTION.MESSAGE_PERMISSION_ERROR, name, originalAction: RPC_ACTION.UNPROVIDE } const msgDeniedProving = { topic: TOPIC.RPC, action: RPC_ACTION.MESSAGE_DENIED, name, originalAction: RPC_ACTION.PROVIDE } const msgDeniedUnproving = { topic: TOPIC.RPC, action: RPC_ACTION.MESSAGE_DENIED, name, originalAction: RPC_ACTION.UNPROVIDE } expectations(permissionErrProvidingMsg) expectations(permissionErrUnprovidingMsg) expectations(msgDeniedProving) expectations(msgDeniedUnproving) handle(permissionErrProvidingMsg) handle(permissionErrUnprovidingMsg) handle(msgDeniedProving) handle(msgDeniedUnproving) }) it('logs unknown correlation error when handling unknown rpc response', () => { const message = { topic: TOPIC.RPC, action: RPC_ACTION.ACCEPT, name, correlationId: '123abc' } services.loggerMock .expects('error') .once() .withExactArgs(message, EVENT.UNKNOWN_CORRELATION_ID) handle(message) }) describe('when providing', () => { beforeEach(() => { rpcHandler.provide(name, rpcProviderSpy as RPCProvider) }) it('doesn\'t register provider twice', () => { services.connectionMock .expects('sendMessage') .never() services.timeoutRegistryMock .expects('add') .never() expect(rpcHandler.provide.bind(rpcHandler, name, rpcProviderSpy as RPCProvider)) .to.throw(`RPC ${name} already registered`) }) it('triggers rpc provider callback in a new request', () => { const message: RPCMessage = { topic: TOPIC.RPC, action: RPC_ACTION.REQUEST, name, parsedData: data, correlationId: '123' } const rpcResponse = new RPCResponse(message, options, services) handle(message) sinon.assert.calledOnce(rpcProviderSpy) sinon.assert.calledWithExactly(rpcProviderSpy, data, rpcResponse) }) it('deregisters providers', () => { const message = { topic: TOPIC.RPC, action: RPC_ACTION.UNPROVIDE, names: [name], correlationId: '1' } services.connectionMock .expects('sendMessage') .once() .withExactArgs(message) services.timeoutRegistryMock .expects('add') .once() .withExactArgs({ message }) rpcHandler.unprovide(name) }) it('doesn\'t send deregister provider message twice', () => { services.connectionMock .expects('sendMessage') .once() services.timeoutRegistryMock .expects('add') .once() services.loggerMock .expects('warn') .once() rpcHandler.unprovide(name) rpcHandler.unprovide(name) }) }) describe('when making', () => { let rpcResponseCallback: sinon.SinonSpy let promise: Promise<any> let rpcPromiseResponseSuccess: sinon.SinonSpy let rpcPromiseResponseFail: sinon.SinonSpy let correlationIdCallbackRpc: string let correlationIdPromiseRpc: string beforeEach(() => { services.timeoutRegistry = new TimeoutRegistry(services, options) rpcResponseCallback = sinon.spy() rpcHandler.make(name, data, rpcResponseCallback) correlationIdCallbackRpc = getLastMessageSent().correlationId as string rpcPromiseResponseSuccess = sinon.spy() rpcPromiseResponseFail = sinon.spy() promise = rpcHandler.make(name, data) promise .then(rpcPromiseResponseSuccess) .catch(rpcPromiseResponseFail) correlationIdPromiseRpc = getLastMessageSent().correlationId as string }) it('handles permission errors', async () => { const action = RPC_ACTION.MESSAGE_PERMISSION_ERROR const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action, name, originalAction: RPC_ACTION.REQUEST, correlationId }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) await PromiseDelay(rpcAcceptTimeout * 2) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.calledWithExactly(rpcResponseCallback, RPC_ACTION[action]) sinon.assert.notCalled(rpcPromiseResponseSuccess) sinon.assert.calledOnce(rpcPromiseResponseFail) sinon.assert.calledWithExactly(rpcPromiseResponseFail, RPC_ACTION[action]) }) it('handles message denied errors', async () => { const action = RPC_ACTION.MESSAGE_DENIED const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action, name, originalAction: RPC_ACTION.REQUEST, correlationId }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) await PromiseDelay(rpcAcceptTimeout * 2) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.calledWithExactly(rpcResponseCallback, RPC_ACTION[action]) sinon.assert.notCalled(rpcPromiseResponseSuccess) sinon.assert.calledOnce(rpcPromiseResponseFail) sinon.assert.calledWithExactly(rpcPromiseResponseFail, RPC_ACTION[action]) }) it('responds rpc with error when request is not accepted in time', async () => { const action = RPC_ACTION.ACCEPT_TIMEOUT const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action, name, correlationId }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) await PromiseDelay(0) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.calledWithExactly(rpcResponseCallback, RPC_ACTION[RPC_ACTION.ACCEPT_TIMEOUT]) sinon.assert.notCalled(rpcPromiseResponseSuccess) sinon.assert.calledOnce(rpcPromiseResponseFail) sinon.assert.calledWithExactly(rpcPromiseResponseFail, RPC_ACTION[RPC_ACTION.ACCEPT_TIMEOUT]) }) it('handles the rpc response accepted message', async () => { const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action: RPC_ACTION.ACCEPT, name, correlationId }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) await PromiseDelay(rpcAcceptTimeout * 2) sinon.assert.notCalled(rpcResponseCallback) sinon.assert.notCalled(rpcPromiseResponseFail) sinon.assert.notCalled(rpcPromiseResponseSuccess) }) it('calls rpcResponse with error when response is not sent in time', async () => { const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action: RPC_ACTION.ACCEPT, name, correlationId }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) handle({ topic: TOPIC.RPC, action: RPC_ACTION.RESPONSE_TIMEOUT, name, correlationId: correlationIdCallbackRpc }) handle({ topic: TOPIC.RPC, action: RPC_ACTION.RESPONSE_TIMEOUT, name, correlationId: correlationIdPromiseRpc }) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.calledWithExactly(rpcResponseCallback, RPC_ACTION[RPC_ACTION.RESPONSE_TIMEOUT]) await PromiseDelay(0) sinon.assert.notCalled(rpcPromiseResponseSuccess) sinon.assert.calledOnce(rpcPromiseResponseFail) sinon.assert.calledWithExactly(rpcPromiseResponseFail, RPC_ACTION[RPC_ACTION.RESPONSE_TIMEOUT]) }) it('calls rpcResponse with error when no rpc provider is returned', async () => { const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action: RPC_ACTION.ACCEPT, name, correlationId }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) handle({ topic: TOPIC.RPC, action: RPC_ACTION.NO_RPC_PROVIDER, name, correlationId: correlationIdCallbackRpc }) handle({ topic: TOPIC.RPC, action: RPC_ACTION.NO_RPC_PROVIDER, name, correlationId: correlationIdPromiseRpc }) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.calledWithExactly(rpcResponseCallback, RPC_ACTION[RPC_ACTION.NO_RPC_PROVIDER]) await PromiseDelay(0) sinon.assert.notCalled(rpcPromiseResponseSuccess) sinon.assert.calledOnce(rpcPromiseResponseFail) sinon.assert.calledWithExactly(rpcPromiseResponseFail, RPC_ACTION[RPC_ACTION.NO_RPC_PROVIDER]) }) it('handles the rpc response RESPONSE message', async () => { const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action: RPC_ACTION.RESPONSE, name, correlationId, parsedData: data }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.calledWithExactly(rpcResponseCallback, null, data) await PromiseDelay(0) sinon.assert.notCalled(rpcPromiseResponseFail) sinon.assert.calledOnce(rpcPromiseResponseSuccess) sinon.assert.calledWithExactly(rpcPromiseResponseSuccess, data) }) it('doesn\'t call rpc response callback twice when handling response message', async () => { const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action: RPC_ACTION.RESPONSE, name, correlationId, parsedData: data }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) handleMessage(correlationIdPromiseRpc) await PromiseDelay(rpcResponseTimeout * 2) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.notCalled(rpcPromiseResponseFail) sinon.assert.calledOnce(rpcPromiseResponseSuccess) }) it('handles the rpc response error message', async () => { const error = 'ERROR' const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action: RPC_ACTION.REQUEST_ERROR, name, correlationId, parsedData: error }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) await PromiseDelay(rpcResponseTimeout * 2) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.calledWithExactly(rpcResponseCallback, error) sinon.assert.notCalled(rpcPromiseResponseSuccess) sinon.assert.calledOnce(rpcPromiseResponseFail) sinon.assert.calledWithExactly(rpcPromiseResponseFail, error) }) it('doesn\'t call rpc response callback twice when handling error message', async () => { const error = 'ERROR' const handleMessage = (correlationId: string) => handle({ topic: TOPIC.RPC, action: RPC_ACTION.REQUEST_ERROR, name, correlationId, parsedData: error }) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdCallbackRpc) handleMessage(correlationIdPromiseRpc) handleMessage(correlationIdPromiseRpc) await PromiseDelay(rpcResponseTimeout * 2) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.notCalled(rpcPromiseResponseSuccess) sinon.assert.calledOnce(rpcPromiseResponseFail) }) it('responds with error when onConnectionLost', async () => { services.simulateConnectionLost() await PromiseDelay(1) sinon.assert.calledOnce(rpcResponseCallback) sinon.assert.calledWithExactly(rpcResponseCallback, EVENT.CLIENT_OFFLINE) sinon.assert.notCalled(rpcPromiseResponseSuccess) sinon.assert.calledOnce(rpcPromiseResponseFail) sinon.assert.calledWithExactly(rpcPromiseResponseFail, EVENT.CLIENT_OFFLINE) }) }) describe('limbo', () => { beforeEach(() => { services.connection.isConnected = false services.connection.isInLimbo = true }) it('returns client offline error once limbo state over', async () => { rpcHandler.make(name, data, rpcMakeSpy) services.simulateExitLimbo() await PromiseDelay(1) sinon.assert.calledOnce(rpcMakeSpy) sinon.assert.calledWithExactly(rpcMakeSpy, EVENT.CLIENT_OFFLINE) }) it('sends messages once re-established if in limbo', async () => { rpcHandler.make(name, data, rpcMakeSpy) services.connectionMock .expects('sendMessage') .once() services.simulateConnectionReestablished() await PromiseDelay(1) }) }) })