411 lines
10 KiB
TypeScript
411 lines
10 KiB
TypeScript
import bodyParser from 'body-parser'
|
|
import express from 'express'
|
|
import request from 'supertest'
|
|
import {Request} from './jsonrpc'
|
|
import {createClient} from './supertest'
|
|
import {ensure} from './ensure'
|
|
import {jsonrpc} from './express'
|
|
import {noopLogger} from './test-utils'
|
|
import {WithContext} from './types'
|
|
|
|
describe('jsonrpc', () => {
|
|
|
|
interface Context {
|
|
userId: number
|
|
}
|
|
|
|
interface Service {
|
|
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>
|
|
|
|
addWithContext(a: number, b: number): number
|
|
addWithContext2(a: number, b: number): Promise<number>
|
|
}
|
|
|
|
const ensureLoggedIn = ensure<Context>(c => !!c.userId)
|
|
|
|
@ensureLoggedIn
|
|
class MyService implements WithContext<Service, Context> {
|
|
constructor(readonly time: number) {}
|
|
add(context: Context, a: number, b: number) {
|
|
return a + b
|
|
}
|
|
multiply(...numbers: number[]) {
|
|
return numbers.reduce((a, b) => a * b, 1)
|
|
}
|
|
delay(): Promise<void> {
|
|
return new Promise(resolve => {
|
|
setTimeout(resolve, this.time)
|
|
})
|
|
}
|
|
syncError(context: Context, message: string) {
|
|
throw new Error(message)
|
|
}
|
|
async asyncError(context: Context, message: string) {
|
|
throw new Error(message)
|
|
}
|
|
async httpError(context: Context, statusCode: number, message: string) {
|
|
const err: any = new Error(message)
|
|
err.statusCode = statusCode
|
|
err.errors = [{
|
|
message: 'one',
|
|
}]
|
|
throw err
|
|
}
|
|
addWithContext = (ctx: Context, a: number, b: number) => {
|
|
return a + b + ctx.userId
|
|
}
|
|
_private = () => {
|
|
return 1
|
|
}
|
|
@ensureLoggedIn
|
|
addWithContext2(ctx: Context, a: number, b: number) {
|
|
return Promise.resolve(a + b + ctx!.userId)
|
|
}
|
|
}
|
|
|
|
let userId: number | undefined = 1000
|
|
function createApp() {
|
|
userId = 1000
|
|
const app = express()
|
|
app.use(bodyParser.json())
|
|
app.use('/',
|
|
jsonrpc(() => ({userId}), noopLogger)
|
|
.addService('/app/myService', new MyService(5), [
|
|
'add',
|
|
'delay',
|
|
'syncError',
|
|
'asyncError',
|
|
'httpError',
|
|
'addWithContext',
|
|
'addWithContext2',
|
|
])
|
|
.router(),
|
|
)
|
|
return app
|
|
}
|
|
|
|
const client = createClient<Service>(createApp(), '/app/myService')
|
|
|
|
async function getError(promise: Promise<unknown>) {
|
|
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('/app/myService')
|
|
.send({
|
|
id: 1,
|
|
jsonrpc: '2.0',
|
|
method: 'syncError',
|
|
params: ['test'],
|
|
})
|
|
.expect(500)
|
|
expect(response.body).toEqual({
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
result: null,
|
|
error: {
|
|
code: -32000,
|
|
data: null,
|
|
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 readable', async () => {
|
|
const result = await request(createApp())
|
|
.post('/app/myService')
|
|
.send('a=1')
|
|
.expect(400)
|
|
expect(result.body.error.message).toEqual('Invalid request')
|
|
})
|
|
it('returns an error when message is not valid', async () => {
|
|
const result = await request(createApp())
|
|
.post('/app/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 result = await client.add(3, 4)
|
|
expect(result).toEqual(3 + 4)
|
|
})
|
|
it('handles promises', async () => {
|
|
const response = await client.delay()
|
|
expect(response).toEqual(undefined)
|
|
})
|
|
it('can use context', async () => {
|
|
const response = await client.addWithContext(5, 7)
|
|
expect(response).toEqual(1000 + 5 + 7)
|
|
})
|
|
it('can use context as extra argument', async () => {
|
|
const response = await client.addWithContext2(5, 7)
|
|
expect(response).toEqual(1000 + 5 + 7)
|
|
})
|
|
it('can validate context using @ensure decorator', async () => {
|
|
userId = undefined
|
|
const err = await getError(client.addWithContext2(5, 7))
|
|
expect(err.message).toMatch(/Invalid request/)
|
|
})
|
|
it('handles synchronous notifications', async () => {
|
|
await request(createApp())
|
|
.post('/app/myService')
|
|
.send({
|
|
jsonrpc: '2.0',
|
|
method: 'add',
|
|
params: [1, 2],
|
|
})
|
|
.expect(204)
|
|
.expect('')
|
|
|
|
await request(createApp())
|
|
.post('/app/myService')
|
|
.send({
|
|
jsonrpc: '2.0',
|
|
id: null,
|
|
method: 'add',
|
|
params: [1, 2],
|
|
})
|
|
.expect(204)
|
|
.expect('')
|
|
})
|
|
})
|
|
|
|
describe('invalid requests', () => {
|
|
it('returns 404 when invalid request method used', async () => {
|
|
await request(createApp())
|
|
.put('/app/myService')
|
|
.send({
|
|
id: 123,
|
|
jsonrpc: '2.0',
|
|
method: 'toString',
|
|
params: [],
|
|
})
|
|
.expect(404)
|
|
})
|
|
|
|
it('returns 404 when service url is invalid', async () => {
|
|
await request(createApp())
|
|
.post('/app/nonExistingService')
|
|
.send({
|
|
id: 123,
|
|
jsonrpc: '2.0',
|
|
method: 'toString',
|
|
params: [],
|
|
})
|
|
.expect(404)
|
|
})
|
|
})
|
|
|
|
describe('multiple services', () => {
|
|
interface S1 {
|
|
add(a: number, b: number): number
|
|
}
|
|
class Test1 implements WithContext<S1, Context> {
|
|
add(c: Context, a: number, b: number) {
|
|
return a + b
|
|
}
|
|
}
|
|
class Test2 implements WithContext<S1, Context> {
|
|
add(c: Context, a: number, b: number): number {
|
|
throw new Error('Not implemented')
|
|
}
|
|
}
|
|
const app = express()
|
|
app.use(bodyParser.json())
|
|
app.get('/app/s3', (req, res) => {
|
|
throw new Error('test s3')
|
|
})
|
|
app.use('/app',
|
|
jsonrpc(() => ({userId: 1}), noopLogger)
|
|
.addService('/s1', new Test1(), ['add'])
|
|
.addService('/s2', new Test2(), ['add'])
|
|
.router(),
|
|
)
|
|
|
|
it('invokes the first service', async () => {
|
|
await request(app)
|
|
.post('/app/s1')
|
|
.send({
|
|
id: 123,
|
|
jsonrpc: '2.0',
|
|
method: 'add',
|
|
params: [1, 2],
|
|
})
|
|
.expect(200)
|
|
.expect({
|
|
jsonrpc: '2.0',
|
|
id: 123,
|
|
result: 3,
|
|
error: null,
|
|
})
|
|
})
|
|
|
|
it('invokes the second service', async () => {
|
|
await request(app)
|
|
.post('/app/s2')
|
|
.send({
|
|
id: 123,
|
|
jsonrpc: '2.0',
|
|
method: 'add',
|
|
params: [1, 2],
|
|
})
|
|
.expect(500)
|
|
.expect({
|
|
jsonrpc: '2.0',
|
|
id: 123,
|
|
result: null,
|
|
error: {
|
|
code: -32000,
|
|
message: 'Server error',
|
|
data: null,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('invokes the second service', async () => {
|
|
await request(app)
|
|
.get('/app/s3')
|
|
.expect(500)
|
|
.expect(/Error: test s3/)
|
|
})
|
|
})
|
|
|
|
describe('security', () => {
|
|
it('cannot call toString method', async () => {
|
|
await request(createApp())
|
|
.post('/app/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 private _-prefixed methods', async () => {
|
|
await request(createApp())
|
|
.post('/app/myService')
|
|
.send({
|
|
id: 123,
|
|
jsonrpc: '2.0',
|
|
method: '_private',
|
|
params: [],
|
|
})
|
|
.expect(404)
|
|
.expect(/Method not found/)
|
|
})
|
|
|
|
it('cannot call any other methods in objects prototype', async () => {
|
|
await request(createApp())
|
|
.post('/app/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,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('cannot call non-idempotent methods using GET request', async () => {
|
|
const params = encodeURIComponent(JSON.stringify([1, 2]))
|
|
await request(createApp())
|
|
.get(`/app/myService?jsonrpc=2.0&id=1&method=add¶ms=${params}`)
|
|
.expect(405)
|
|
})
|
|
|
|
describe('hook', () => {
|
|
|
|
let requests: Request[] = []
|
|
let results: any[] = []
|
|
function create() {
|
|
requests = []
|
|
results = []
|
|
|
|
userId = 1000
|
|
const app = express()
|
|
const myService = new MyService(5)
|
|
app.use(bodyParser.json())
|
|
app.use('/app',
|
|
jsonrpc(
|
|
() => Promise.resolve({userId}),
|
|
noopLogger,
|
|
async (details, makeRequest) => {
|
|
requests.push(details.request)
|
|
const result = await makeRequest()
|
|
results.push(result)
|
|
return result
|
|
},
|
|
)
|
|
.addService('/myService', myService)
|
|
.router(),
|
|
)
|
|
return app
|
|
}
|
|
|
|
it('should wrap POST rpc method call', async () => {
|
|
const req = {
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
method: 'add',
|
|
params: [1, 2],
|
|
}
|
|
const response = await request(create())
|
|
.post('/app/myService')
|
|
.send(req)
|
|
|
|
expect(response.body.result).toEqual(3)
|
|
expect(requests).toEqual([ req ])
|
|
expect(results).toEqual([ response.body ])
|
|
})
|
|
|
|
})
|
|
})
|
|
|
|
})
|