From 69ee40b21c08302adb4a4da57dc6ede3f9161a23 Mon Sep 17 00:00:00 2001 From: CheerlessCloud Date: Fri, 28 Sep 2018 08:50:52 +0300 Subject: [PATCH] feat(rpc/handler): add initial implementation Handler class and basic positive tests --- src/rpc/Handler.js | 77 ++++++++++++++++++++++++++ src/rpc/Handler.test.js | 116 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/rpc/Handler.js create mode 100644 src/rpc/Handler.test.js diff --git a/src/rpc/Handler.js b/src/rpc/Handler.js new file mode 100644 index 0000000..fedd0b0 --- /dev/null +++ b/src/rpc/Handler.js @@ -0,0 +1,77 @@ +/* eslint-disable no-empty-function,no-unused-vars */ +// @flow +import { type IMessage } from '../AMQPMessage'; +import RpcService from '../Service'; +import errorToObject from './errorToObject'; + +export default class RpcHandler { + +_service: RpcService; + +_message: IMessage; + +context: Object = {}; + + get action(): string { + return 'default'; + } + + get payload(): $PropertyType { + return this._message.payload; + } + + constructor({ service, message }: { service: RpcService, message: IMessage }) { + this._service = service; + this._message = message; + + if (this.handle === RpcHandler.prototype.handle) { + throw new Error('You must override handle method'); + } + } + + async reply({ payload, error }: { payload?: ?Object, error?: Error }) { + const { messageId, correlationId, replyTo } = this._message._props; + if (!replyTo) { + return; + } + + const adapter = this._service._getAdapter(); + await adapter.send( + replyTo, + { + error: errorToObject(error), + payload, + }, + { messageId, correlationId }, + ); + } + + async beforeHandle() {} + + async handle(): ?Object {} + + handleFail(err: Error) { + // reply + // reject + } + + async handleSuccess(replyPayload: ?Object) { + await this.reply({ payload: replyPayload }); + await this._message.ack(); + } + + async onSuccess() {} + async onFail(error: Error) {} + async afterHandle() {} + + async execute() { + try { + await this.beforeHandle(); + const replyPayload = await this.handle(); + await this.handleSuccess(replyPayload); + await this.onSuccess(); + } catch (err) { + await this.handleFail(err); + await this.onFail(err); + } finally { + await this.afterHandle(); + } + } +} diff --git a/src/rpc/Handler.test.js b/src/rpc/Handler.test.js new file mode 100644 index 0000000..0767435 --- /dev/null +++ b/src/rpc/Handler.test.js @@ -0,0 +1,116 @@ +/* eslint-disable no-param-reassign */ +// @flow +import test from 'ava'; +import { spy, stub } from 'sinon'; +import uuid from 'uuid/v4'; +import Handler from './Handler'; + +test.beforeEach(t => { + t.context = {}; + t.context.reply = { foo: 42 }; + t.context.AwesomeHandler = class AwesomeHandler extends Handler { + async handle() { + return t.context.reply; + } + }; + t.context.adapterSendStub = stub().resolves(undefined); + t.context.serviceStub = { + _getAdapter: () => ({ + send: t.context.adapterSendStub, + }), + }; + t.context.messageStub = { + id: uuid(), + _props: { + messageId: uuid(), + correlationId: uuid(), + replyTo: uuid(), + }, + payload: { foo: 'bar' }, + ack: stub().resolves(undefined), + reject: stub().resolves(undefined), + }; +}); + +test('construct handler', async t => { + const { AwesomeHandler, serviceStub, messageStub } = t.context; + t.notThrows( + () => + new AwesomeHandler({ + service: serviceStub, + message: messageStub, + }), + ); +}); + +test('positive execute case', async t => { + const { AwesomeHandler, serviceStub, messageStub } = t.context; + const handler = new AwesomeHandler({ + service: serviceStub, + message: messageStub, + }); + handler.handleFail = err => t.fail(err); + + const beforeHandleSpy = spy(handler, 'beforeHandle'); + const handleSpy = spy(handler, 'handle'); + const afterHandleSpy = spy(handler, 'afterHandle'); + const handleFailSpy = spy(handler, 'handleFail'); + const handleSuccessSpy = spy(handler, 'handleSuccess'); + const onFailSpy = spy(handler, 'onFail'); + const onSuccessSpy = spy(handler, 'onSuccess'); + + await t.notThrows(handler.execute()); + + t.true(beforeHandleSpy.calledOnce); + t.true(beforeHandleSpy.calledBefore(handleSpy)); + + t.true(handleSpy.calledOnce); + t.true(handleSpy.calledBefore(handleSuccessSpy)); + + t.false(handleFailSpy.called); + t.false(onFailSpy.called); + + t.true(handleSuccessSpy.calledOnce); + t.true(handleSuccessSpy.calledBefore(onSuccessSpy)); + t.true(onSuccessSpy.calledOnce); + t.true(onSuccessSpy.calledBefore(afterHandleSpy)); + + t.true(messageStub.ack.calledOnce); + t.true(messageStub.ack.calledBefore(onSuccessSpy)); + t.false(messageStub.reject.called); + + t.true(afterHandleSpy.calledOnce); +}); + +test('correct reply at positive execute case', async t => { + const { AwesomeHandler, serviceStub, messageStub } = t.context; + const handler = new AwesomeHandler({ + service: serviceStub, + message: messageStub, + }); + handler.handleFail = err => t.fail(err); + + const handleSuccessSpy = spy(handler, 'handleSuccess'); + const replySpy = spy(handler, 'reply'); + + await t.notThrows(handler.execute()); + + t.true(handleSuccessSpy.calledOnceWith(t.context.reply)); + t.true(replySpy.calledOnceWith({ payload: t.context.reply })); + t.true(t.context.adapterSendStub.calledOnce); + t.true(messageStub.ack.calledOnce); + t.false(messageStub.reject.called); +}); + +test('must override handle method', async t => { + const { serviceStub, messageStub } = t.context; + const HandlerClass = class AwesomeHandler2 extends Handler {}; + t.throws( + () => + new HandlerClass({ + service: serviceStub, + message: messageStub, + }), + 'You must override handle method', + ); +});