diff --git a/packages/jsonrpc/src/server.test.ts b/packages/jsonrpc/src/server.test.ts index f5e429e..ed1c00a 100644 --- a/packages/jsonrpc/src/server.test.ts +++ b/packages/jsonrpc/src/server.test.ts @@ -5,7 +5,15 @@ import bodyParser from 'body-parser' describe('jsonrpc', () => { - class Service { + interface IService { + add(a: number, b: number): number + delay(): Promise + syncError(message: string): void + asyncError(message: string): Promise + httpError(statusCode: number, message: string): Promise + } + + class Service implements IService { constructor(readonly time: number) {} add(a: number, b: number) { return a + b @@ -13,63 +21,141 @@ describe('jsonrpc', () => { multiply(...numbers: number[]) { return numbers.reduce((a, b) => a * b, 1) } - delay() { + delay(): Promise { return new Promise(resolve => { setTimeout(resolve, this.time) }) } + syncError(message: string) { + throw new Error(message) + } + async asyncError(message: string) { + throw new Error(message) + } + async httpError(statusCode: number, message: string) { + const err: any = new Error(message) + err.statusCode = statusCode + err.errors = [{ + message: 'one', + }] + throw err + } } function createApp() { const app = express() app.use(bodyParser.json()) - app.use('/myService', jsonrpc(new Service(5), ['add', 'delay'])) - app.use((err: any, req: any, res: any, next: any) => { - console.log(err) - next(err) - }) + app.use('/myService', jsonrpc(new Service(5), [ + 'add', + 'delay', + 'syncError', + 'asyncError', + 'httpError', + ])) return app } - describe('errors', () => { + type ArgumentTypes = T extends (...args: infer U) => infer R ? U: never + type RetType = T extends (...args: any[]) => infer R ? R : never + type RetProm = T extends Promise ? T : Promise + type PromisifyReturnType = (...a: ArgumentTypes) => RetProm> + type Asyncified = { + [K in keyof T]: PromisifyReturnType + } + function createClient(app: express.Application) { + let id = 0 + const proxy = new Proxy({}, { + get(obj, prop) { + id++ + return async function makeRequest(...args: any[]) { + const result = await request(app) + .post('/myService') + .send({ + jsonrpc: '2.0', + id, + method: prop, + params: args, + }) + const {body} = result + if (body.error) { + throw body.error + } + return body.result + } + }, + }) + return proxy as Asyncified + } + + const client = createClient(createApp()) + + async function getError(promise: Promise) { + let error + try { + await promise + } catch (err) { + error = err + } + expect(error).toBeTruthy() + return error + } + + describe('errors', () => { + it('handles sync errors', async () => { + const response = await request(createApp()) + .post('/myService') + .send({ + id: 1, + jsonrpc: '2.0', + method: 'syncError', + params: ['test'], + }) + .expect(500) + expect(response.body.id).toEqual(1) + expect(response.body).toEqual({ + jsonrpc: '2.0', + id: 1, + result: null, + error: { + code: -32000, + message: 'Server error', + }, + }) + }) + it('handles async errors', async () => { + const err = await getError(client.asyncError('test')) + expect(err.message).toBe('Server error') + expect(err.code).toBe(-32000) + }) + it('returns an error when message is not in json format', async () => { + const result = await request(createApp()) + .post('/myService') + .send('a=1') + .expect(400) + expect(result.body.error.message).toEqual('Parse error') + }) + it('returns an error when message is not valid', async () => { + const result = await request(createApp()) + .post('/myService') + .send({}) + .expect(400) + expect(result.body.error.message).toEqual('Invalid Request') + }) + it('converts http errors into jsonrpc errors', async () => { + const err = await getError(client.httpError(403, 'Unauthorized')) + expect(err.message).toEqual('Unauthorized') + }) }) describe('success', () => { it('can call method and receive results', async () => { - const response = await request(createApp()) - .post('/myService') - .send({ - jsonrpc: '2.0', - id: 1, - method: 'add', - params: [1, 2], - }) - .expect(200) - - expect(response.body).toEqual({ - jsonrpc: '2.0', - id: 1, - result: 3, - error: null, - }) + const result = await client.add(3, 4) + expect(result).toEqual(3 + 4) }) it('handles promises', async () => { - const response = await request(createApp()) - .post('/myService') - .send({ - jsonrpc: '2.0', - id: 1, - method: 'delay', - params: [], - }) - .expect(200) - expect(response.body).toEqual({ - jsonrpc: '2.0', - id: 1, - // result: null, - error: null, - }) + const response = await client.delay() + expect(response).toEqual(undefined) }) it('handles synchronous notifications', async () => { await request(createApp()) @@ -96,12 +182,48 @@ describe('jsonrpc', () => { }) describe('security', () => { - it('cannot call toString method', () => { - + it('cannot call toString method', async () => { + await request(createApp()) + .post('/myService') + .send({ + id: 123, + jsonrpc: '2.0', + method: 'toString', + params: [], + }) + .expect(404) + .expect({ + jsonrpc: '2.0', + id: 123, + result: null, + error: { + code: -32601, + message: 'Method not found', + data: null, + }, + }) }) - it('cannot call any other methods in objects prototype', () => { - + it('cannot call any other methods in objects prototype', async () => { + await request(createApp()) + .post('/myService') + .send({ + id: 123, + jsonrpc: '2.0', + method: '__defineGetter__', + params: [], + }) + .expect(404) + .expect({ + jsonrpc: '2.0', + id: 123, + result: null, + error: { + code: -32601, + message: 'Method not found', + data: null, + }, + }) }) }) diff --git a/packages/jsonrpc/src/server.ts b/packages/jsonrpc/src/server.ts index c1a6b9d..128bff3 100644 --- a/packages/jsonrpc/src/server.ts +++ b/packages/jsonrpc/src/server.ts @@ -116,7 +116,7 @@ export function jsonrpc>( !Array.isArray(params) ) { res.status(400) - return res.json(createErrorResponse(null, ERROR_INVALID_REQUEST)) + return res.json(createErrorResponse(id, ERROR_INVALID_REQUEST)) } if ( @@ -124,14 +124,20 @@ export function jsonrpc>( typeof (rpcService as any)[method] !== 'function' ) { res.status(404) - return res.json(createErrorResponse(null, ERROR_METHOD_NOT_FOUND)) + return res.json(createErrorResponse(id, ERROR_METHOD_NOT_FOUND)) } // if (rpcService[method].arguments.length !== params.length) { // return res.json(createErrorResponse(null, ERROR_INVALID_PARAMS)) // } - const retValue = (rpcService as any)[method](...params) + // TODO handle synchronous errors + let retValue + try { + retValue = (rpcService as any)[method](...params) + } catch (err) { + return handleError(err, req, res, next) + } if (!isPromise(retValue)) { if (isNotification) {