import { describe, it, Func } from "mocha"; import { RPCServer, RPCSocket, SubscriptionResponse, makeSubResponse } from '../Index' import * as uuidv4 from "uuid/v4" import { doesNotReject } from "assert"; import { Socket } from "dgram"; const add = (...args:number[]) => {return args.reduce((a,b)=>a+b, 0)} function makeServer(){ let subcallback return new RPCServer<{ topic: string }>(21000, [{ name: "test", exportRPCs: () => [ { name: 'echo', call: async (s:string) => s, },{ name: 'simpleSubscribe', hook: async(callback) => { subcallback = callback return makeSubResponse<{topic: string}>({topic: "test"}) } },{ name: 'subscribe', hook: async (callback) => { subcallback = callback return makeSubResponse<{topic: string}>({topic: "test"}) }, onClose: (res, rpc) => { console.log("onClose", rpc.name === 'subscribe' && res?"OK":"") subcallback = null }, onCallback: (...args:any) => { console.log("onCallback", args[0] === "test" && args[1] === "callback"?"OK":"") } }, add, function triggerCallback(...messages:any[]):number {return subcallback.apply({}, messages)}, ] }],{ connectionHandler: (socket) => { }, closeHandler: (socket) => { }, errorHandler: (socket, err) => { throw err } }) } describe('RPCServer', () => { let server: RPCServer<{ topic: string }, any> before(() => { server = makeServer() }) after(() => { server.destroy() }) it('should be able to use all kinds of RPC definitions', (done) => { const echo = (x) => x const server = new RPCServer(21003, [{ name: 'HelloWorldRPCGroup', exportRPCs: () => [ echo, //named function variable function echof(x){ return x }, //named function { name: 'echoExplicit', //describing object call: async (x,y,z) => [x,y,z] } ] }]) const client = new RPCSocket(21003, 'localhost') client.connect().then(async () => { const r0 = await client['HelloWorldRPCGroup'].echo('Hello') const r1 = await client['HelloWorldRPCGroup'].echof('World') const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R','P','C!') if(r0 === 'Hello' && r1 === 'World' && r2.join('') ==='RPC!'){ client.destroy() server.destroy() done() } }) }) it('new RPCServer() should fail on bad RPC', (done) => { try{ new RPCServer(20001, [{ name: "bad", exportRPCs: () => [ (aaa,bbb,ccc) => { return aaa+bbb+ccc } ] }]) done(new Error("Didn't fail with bad RPC")) }catch(badRPCError){ done() } }) }) describe('RPCSocket', () => { let client: RPCSocket let server: RPCServer<{topic: string}> before(async() => { server = makeServer() client = new RPCSocket(21000, "localhost") return await client.connect() }) after(() => { client.destroy() server.destroy() }) it('should have rpc echo', (done) => { client['test'].echo("x").then(x => { if(x === 'x') done() else done(new Error('echo RPC response did not match')) }) }) it('should add up to 6', (done) => { client['test'].add(1,2,3).then(x => { if(x === 6) done() else done(new Error('add RPC response did not match')) }) }) it('should subscribe with success', (done) => { client['test'].simpleSubscribe(console.log).then(res => { if(res.result === 'Success'){ done() }else{ console.error(res) done(new Error('Subscribe did not return success')) } }) }) it('subscribe should call back', (done) => { client['test'].subscribe((...args: any) => { if(args[0] === "test" && args[1] === "callback") done() else done(new Error("Bad callback value "+ args)) }).then( async () => { await client['test'].triggerCallback("test", "callback") }) }) it('simpleSubscribe should call back', (done) => { client['test'].simpleSubscribe((...args: any) => { if(args[0] === "test_" && args[1] === "callback_") done() else done(new Error("Bad callback value "+ args)) }).then( async () => { await client['test'].triggerCallback("test_", "callback_") }) }) }) describe('It should do unhook', () => { let candy = "OK" let cb: Function let client: RPCSocket let server: RPCServer<{topic: string}> before(async() => { server = new RPCServer<{ topic: string }>(21000, [{ name: "test", exportRPCs: () => [{ name: 'subscribe', hook: async(callback):Promise> => { cb = callback return { result: "Success", uuid: uuidv4(), topic: "test" } } }, function checkCandy():string { cb(candy); return candy }, function stealCandy():string { candy = "_OK"; cb(candy); cb = () => {}; return candy } ] }],{ connectionHandler: (socket) => { }, closeHandler: (socket) => { }, errorHandler: (socket, err) => { throw err } }) client = new RPCSocket(21000, "localhost") return await client.connect() }) after(() => { client.destroy() server.destroy() }) it('Unhook+unsubscribe should stop callbacks', (done) => { client['test'].subscribe(c => {}).then( async (res: SubscriptionResponse) => { const r1 = await client['test'].checkCandy() const r3 = await client['test'].stealCandy() client.unhook(res.uuid) const r2 = await client['test'].checkCandy() const r4 = await client['test'].checkCandy() if(r1 === "OK" && r3 === "_OK" && r2 === "_OK" && r4 === "_OK") done() else done(new Error("Results did not match: "+[r1,r2,r3,r4])) }) }) }) type SesameTestIfc = { test: { checkCandy: ()=>Promise subscribe: (callback) => Promise> } } describe('Sesame should unlock the socket', () => { let candy = "OK" let client: RPCSocket & SesameTestIfc let server: RPCServer let cb = (...args) => {} before((done) => { server = new RPCServer(21004, [{ name: "test", exportRPCs: () => [ { name: 'subscribe', hook: async(callback) => { cb = callback return { result: "Success", uuid: uuidv4(), topic: 'test' } } }, async function checkCandy():Promise { cb(candy); cb=()=>{}; return candy }, async function manyParams(a,b,c,d) {return [a,b,c,d]} ]} ],{ sesame: (_sesame) => _sesame === 'sesame!' }) const sock = new RPCSocket(21004, "localhost") sock.connect('sesame!').then(cli => { client = cli done() }) }) after(() => { client.destroy() server.destroy() }) it('should work with sesame', (done) => { client.test.checkCandy().then(c => done()) }) it('should work with multiple params', (done) => { client.test['manyParams']('a','b','c','d').then(c => { if(c[0] == 'a' && c[1] === 'b' && c[2] === 'c' && c[3] === 'd') done() }) }) it('should not work without sesame', (done) => { const sock = new RPCSocket(21004, "localhost") sock.connect( /* no sesame */).then(async (cli) => { cli.test.checkCandy().then(d => { done(d) }).catch(e => { //console.log("EXPECTED CLIENT EXCEPTION", String(e)); done() }).finally(() => { cli.destroy() sock.destroy() }) }) }) it('should fail with wrong sesame', (done) => { const sock = new RPCSocket(21004, "localhost") sock.connect('abasd').then(async (cli) => { cli.test.checkCandy().then(d => { done("should not be able to get candy") }).catch(e => { //console.log("EXPECTED CLIENT EXCEPTION", String(e)); done() }).finally(() => { sock.destroy() cli.destroy() }) }) }) it('callback should work with sesame', (done) => { client.test.subscribe((c) => { if(c === candy){ done() } }).then(d => { if(d.result !== 'Success') done('unexpected valid response') client.test.checkCandy() }) }) it('callback should not work without sesame', (done) => { const sock = new RPCSocket(21004, "localhost") sock.connect( /* no sesame */).then(async (cli) => { cli.test.subscribe((c) => { console.log("CALLBACK TRIGGERED UNEXPECTED"); if(c === candy) done("super not") }).then(async d => { await client.test.checkCandy() if(d == null){ done() }else done('unexpected valid response '+(d) ) }).finally(() => { sock.destroy() }) }) }) }) describe('Error handling', ()=>{ let createUser = async( user: {a:any,b:any}) => { throw new Error("BAD BAD BAD") } it("RPC throws on client without handler", (done)=>{ let server = new RPCServer(21004, [ { name: 'createUser' as 'createUser', exportRPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }]}], { }) let sock = new RPCSocket(21004, 'localhost') sock.connect().then((cli) => { cli["createUser"]["createUser"]({ a:'a', b:'b' }) .then(r => { if(r != null) done("UNEXPECTED RESULT " + r) }) .catch((e) => { //console.log("EXPECTED CLIENT EXCEPTION", String(e)); done() }) .finally(() => { cli.destroy() sock.destroy() server.destroy() }) }) }) it("RPC throws on server with handler", (done)=>{ let server = new RPCServer(21004, [ { name: 'createUser' as 'createUser', exportRPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }]}], { errorHandler: (socket, e, rpcName, args) => { done() } }) let sock = new RPCSocket(21004, 'localhost') sock.connect().then((cli) => { cli["createUser"]["createUser"]({ a:'a', b:'b' }) .then(r => { if(r != null) done("UNEXPECTED RESULT " + r) }) .catch((e) => { done("UNEXPECTED CLIENT ERROR " + e) done(e) }) .finally(() => { cli.destroy() sock.destroy() server.destroy() }) }) }) }) describe("Errorhandler functionality", ()=>{ let createUser = async( user: {a:any,b:any}) => { throw new Error("BAD BAD BAD") } it("correct values are passed to the handler", (done)=>{ let server = new RPCServer(21004, [ { name: 'createUser' as 'createUser', exportRPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }]}], { errorHandler: (socket, e, rpcName, args) => { if(e.message === "BAD BAD BAD" && rpcName === "createUser" && args[0]['a'] === 'a' && args[0]['b'] === 'b') done() } }) let sock = new RPCSocket(21004, 'localhost') sock.connect().then((cli) => { cli["createUser"]["createUser"]({ a:'a', b:'b' }) .then(r => { if(r != null) done("UNEXPECTED RESULT " + r) }) .catch((e) => { done("UNEXPECTED CLIENT ERROR " + e) done(e) }) .finally(() => { cli.destroy() sock.destroy() server.destroy() }) }) }) it("handler sees sesame", (done)=>{ let sesame = "AAAAAAAAAAAAAAA" let server = new RPCServer(21004, [ { name: 'createUser' as 'createUser', exportRPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }]}], { sesame: sesame, errorHandler: (socket, e, rpcName, args) => { if(e.message === "BAD BAD BAD" && rpcName === "createUser" && args[0] === sesame && args[1]['a'] === 'a' && args[1]['b'] === 'b') done() } }) let sock = new RPCSocket(21004, 'localhost') sock.connect(sesame).then((cli) => { cli["createUser"]["createUser"]({ a:'a', b:'b' }) .then(r => { if(r != null) done("UNEXPECTED RESULT " + r) }) .catch((e) => { done("UNEXPECTED CLIENT ERROR " + e) done(e) }) .finally(() => { cli.destroy() sock.destroy() server.destroy() }) }) }) })