Add 100% test coverage for jsonrpc server
This commit is contained in:
parent
fdfe356ded
commit
e16edd5655
@ -5,7 +5,15 @@ import bodyParser from 'body-parser'
|
|||||||
|
|
||||||
describe('jsonrpc', () => {
|
describe('jsonrpc', () => {
|
||||||
|
|
||||||
class Service {
|
interface IService {
|
||||||
|
add(a: number, b: number): number
|
||||||
|
delay(): Promise<void>
|
||||||
|
syncError(message: string): void
|
||||||
|
asyncError(message: string): Promise<void>
|
||||||
|
httpError(statusCode: number, message: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
class Service implements IService {
|
||||||
constructor(readonly time: number) {}
|
constructor(readonly time: number) {}
|
||||||
add(a: number, b: number) {
|
add(a: number, b: number) {
|
||||||
return a + b
|
return a + b
|
||||||
@ -13,63 +21,141 @@ describe('jsonrpc', () => {
|
|||||||
multiply(...numbers: number[]) {
|
multiply(...numbers: number[]) {
|
||||||
return numbers.reduce((a, b) => a * b, 1)
|
return numbers.reduce((a, b) => a * b, 1)
|
||||||
}
|
}
|
||||||
delay() {
|
delay(): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(resolve, this.time)
|
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() {
|
function createApp() {
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(bodyParser.json())
|
app.use(bodyParser.json())
|
||||||
app.use('/myService', jsonrpc(new Service(5), ['add', 'delay']))
|
app.use('/myService', jsonrpc(new Service(5), [
|
||||||
app.use((err: any, req: any, res: any, next: any) => {
|
'add',
|
||||||
console.log(err)
|
'delay',
|
||||||
next(err)
|
'syncError',
|
||||||
})
|
'asyncError',
|
||||||
|
'httpError',
|
||||||
|
]))
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('errors', () => {
|
type ArgumentTypes<T> = T extends (...args: infer U) => infer R ? U: never
|
||||||
|
type RetType<T> = T extends (...args: any[]) => infer R ? R : never
|
||||||
|
type RetProm<T> = T extends Promise<any> ? T : Promise<T>
|
||||||
|
type PromisifyReturnType<T> = (...a: ArgumentTypes<T>) => RetProm<RetType<T>>
|
||||||
|
type Asyncified<T> = {
|
||||||
|
[K in keyof T]: PromisifyReturnType<T[K]>
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClient<T>(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<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createClient<IService>(createApp())
|
||||||
|
|
||||||
|
async function getError(promise: Promise<void>) {
|
||||||
|
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', () => {
|
describe('success', () => {
|
||||||
it('can call method and receive results', async () => {
|
it('can call method and receive results', async () => {
|
||||||
const response = await request(createApp())
|
const result = await client.add(3, 4)
|
||||||
.post('/myService')
|
expect(result).toEqual(3 + 4)
|
||||||
.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,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
it('handles promises', async () => {
|
it('handles promises', async () => {
|
||||||
const response = await request(createApp())
|
const response = await client.delay()
|
||||||
.post('/myService')
|
expect(response).toEqual(undefined)
|
||||||
.send({
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
id: 1,
|
|
||||||
method: 'delay',
|
|
||||||
params: [],
|
|
||||||
})
|
|
||||||
.expect(200)
|
|
||||||
expect(response.body).toEqual({
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
id: 1,
|
|
||||||
// result: null,
|
|
||||||
error: null,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
it('handles synchronous notifications', async () => {
|
it('handles synchronous notifications', async () => {
|
||||||
await request(createApp())
|
await request(createApp())
|
||||||
@ -96,12 +182,48 @@ describe('jsonrpc', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('security', () => {
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export function jsonrpc<T, F extends FunctionPropertyNames<T>>(
|
|||||||
!Array.isArray(params)
|
!Array.isArray(params)
|
||||||
) {
|
) {
|
||||||
res.status(400)
|
res.status(400)
|
||||||
return res.json(createErrorResponse(null, ERROR_INVALID_REQUEST))
|
return res.json(createErrorResponse(id, ERROR_INVALID_REQUEST))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -124,14 +124,20 @@ export function jsonrpc<T, F extends FunctionPropertyNames<T>>(
|
|||||||
typeof (rpcService as any)[method] !== 'function'
|
typeof (rpcService as any)[method] !== 'function'
|
||||||
) {
|
) {
|
||||||
res.status(404)
|
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) {
|
// if (rpcService[method].arguments.length !== params.length) {
|
||||||
// return res.json(createErrorResponse(null, ERROR_INVALID_PARAMS))
|
// 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 (!isPromise(retValue)) {
|
||||||
if (isNotification) {
|
if (isNotification) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user