import { describe, it } from "mocha"; import { RPCServer, RPCSocket } from '../Index' import { RPCExporter } from "../src/Interfaces"; import { ConnectedSocket } from "../src/Types"; import * as log from 'why-is-node-running'; import * as http from 'http'; import * as express from 'express'; import * as fetch from 'node-fetch'; const add = (...args: number[]) => { return args.reduce((a, b) => a + b, 0) } function makeServer() { let subcallback const sv = new RPCServer([{ name: 'test', RPCs: [ { name: 'echo', call: async (s: string) => s, }, { name: 'simpleSubscribe', hook: async (callback) => { subcallback = callback return { topic: "test" } }, onClose: (res) => { } }, { name: 'subscribe', hook: async (callback) => { subcallback = callback return { 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 } }) sv.listen(21010) return sv } describe('RPCServer', () => { let client, server const echo = (x) => x before(done => { server = new RPCServer([{ name: 'HelloWorldRPCGroup', RPCs: () => [ echo, //named function variable function echof(x) { return x }, //named function { name: 'echoExplicit', //describing object call: async (x, y, z) => [x, y, z] } ] }]) server.listen(21003) client = new RPCSocket(21003, 'localhost') done() }) after(done => { client.close() server.close() done() }) it('should be able to use all kinds of RPC definitions', (done) => { 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!') { done() } else { done(new Error("Bad response")) } }) }) it('new RPCServer() should fail on bad RPC', (done) => { try { const sv = new RPCServer([{ name: 'bad', RPCs: () => [ (aaa, bbb, ccc) => { return aaa + bbb + ccc } ] }]) sv.listen(20001) done(new Error("Didn't fail with bad RPC")) } catch (badRPCError) { done() } }) }) describe('RPCServer with premade http server', () => { const echo = (x) => x const RPCs = [ echo, //named function variable function echof(x) { return x }, //named function { name: 'echoExplicit', //describing object call: async (x, y, z) => [x, y, z] } ] const RPCExporters = [ { name: 'HelloWorldRPCGroup', RPCs: RPCs, } ] const RPCExporters2 = [ { name: 'Grp2', RPCs: [ function test(){ return "test" } ], } ] let client:RPCSocket, server:RPCServer before(done => { const expressServer = express() const httpServer = new http.Server(expressServer) expressServer.get('/REST_ping', (req, res)=>{ return res .send('REST_pong') .status(200) }) server = new RPCServer( RPCExporters, ) server.attach(httpServer) httpServer.listen(8080) client = new RPCSocket(8080, 'localhost') done() }) after(done => { client.close() server.close() done() }) it('should serve REST', (done) => { fetch('http://localhost:8080/REST_ping').then(response => { response.text().then(text => { if(text === "REST_pong") done() else done(new Error("REST repsonse was "+text)) }) }) }) it('should be able to use all kinds of RPC definitions', (done) => { 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!') { done() } else { done(new Error("Bad response")) } }) }) }) describe('RPCSocket', () => { let client: RPCSocket let server: RPCServer before(async () => { server = makeServer() client = new RPCSocket(21010, "localhost") return await client.connect() }) after(() => { client.close() server.close() }) 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.topic === 'test') { 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', () => { const yesCandy = "OK" const noCandy = "stolen" let candy = yesCandy let cb: Function let cb2: Function let client: RPCSocket let server: RPCServer before(async () => { server = new RPCServer([{ name: "test", RPCs: () => [{ name: 'subscribe', hook: async (callback): Promise => { cb = callback return } }, { name: 'subscribeWithParam', hook: async (param, callback): Promise<{ uuid: string }> => { if (param != "OK") { console.log("param was" + param); return { uuid: "no", } } cb2 = callback return { uuid: "OK", } } }, function publish(): string { cb(candy); return candy }, function unsubscribe(): string { candy = noCandy; cb(candy); cb = () => { }; return candy } ] }], { connectionHandler: (socket) => { }, closeHandler: (socket) => { }, errorHandler: (socket, err) => { throw err } }) server.listen(21010) client = new RPCSocket(21010, "localhost") return await client.connect() }) after(() => { client.close() server.close() }) it('Subscribe with param', (done) => { client['test'].subscribeWithParam("OK", c => { }).then(async (res) => { if (res.uuid === candy) { done() } else done(new Error("Results did not match " + res.uuid)) }) }) let run = 0 const expected = [yesCandy, noCandy, noCandy, noCandy] it('Unhook+unsubscribe should stop callbacks', (done) => { client['test'].subscribe(function myCallback(c) { if (run == 1) (myCallback as any).destroy() if (c !== expected[run++]) { done(new Error(`Wrong candy '${c}' in iteration '${run - 1}'`)) } }).then(async function (res) { const r1 = await client['test'].publish() const r3 = await client['test'].unsubscribe() const r2 = await client['test'].publish() const r4 = await client['test'].publish() if (r1 === yesCandy && r3 === noCandy && r2 === noCandy && r4 === noCandy) done() else done(new Error("Results did not match: " + [r1, r2, r3, r4])) }) }) }) type topicDTO = { topic: string; } type SesameTestIfc = { test: { checkCandy: () => Promise subscribe: (callback: Function) => Promise manyParams: (a: A, b: B, c: C, d: D) => Promise<[A, B, C, D]> } other: { echo: (x: any) => Promise } } describe('Sesame should unlock the socket', () => { let candy = "OK" let client: ConnectedSocket let server: RPCServer let cb: Function = (...args) => { } before((done) => { server = new RPCServer([{ name: "test", RPCs: () => [ { name: 'subscribe', hook: async (callback) => { cb = callback return { topic: 'test' } }, onClose: (a) => { } }, async function checkCandy() { cb(candy); cb = () => { }; return candy }, async function manyParams(a, b, c, d) { return [a, b, c, d] } ], }, { name: 'other', RPCs: () => [ async function echo(x) { return x } ] }], { sesame: (_sesame) => _sesame === 'sesame!' }) server.listen(21004) const sock = new RPCSocket(21004, "localhost") sock.connect('sesame!').then(cli => { client = cli done() }) }) after(() => { client.close() server.close() }) 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) => { if (!cli.test) done() else { done(new Error("Function supposed to be removed without sesame")) } cli.close() sock.close() }) }) it('should fail with wrong sesame', (done) => { const sock = new RPCSocket(21004, "localhost") sock.connect('abasd').then(async (cli) => { if (!cli.test) done() else { done(new Error("Function supposed to be removed without sesame")) } cli.close() sock.close() }) }) it('callback should work with sesame', (done) => { client.test.subscribe((c) => { if (c === candy) { done() } }).then(d => { if (d.topic !== 'test') done('unexpected invalid response') client.test.checkCandy() }) }) }) describe('Error handling', () => { const errtxt = "BAD BAD BAD" let createUser = async (user: { a: any, b: any }) => { throw new Error(errtxt) } it("RPC throws on client without handler", (done) => { let server = new RPCServer([{ name: "createUser", RPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }] }]) server.listen(21004) let sock = new RPCSocket(21004, 'localhost') sock.connect().then((cli) => { cli["createUser"]["createUser"]({ a: 'a', b: 'b' }) .then(r => { if (r != null) done(new Error("UNEXPECTED RESULT " + r)) }) .catch((e) => { if (e.message === errtxt) done() else done(e) }) .finally(() => { cli.close() sock.close() server.close() }) }) }) it("RPC throws on server with handler", (done) => { let server = new RPCServer([{ name: "createUser", RPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }] }], { errorHandler: (socket, e, rpcName, args) => { done() } }) server.listen(21004) 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.close() sock.close() server.close() }) }) }) }) describe("Errorhandler functionality", () => { const errtxt = "BAD BAD BAD" let createUser = async (user: { a: any, b: any }) => { throw new Error(errtxt) } it("correct values are passed to the handler", (done) => { let server = new RPCServer([{ name: "createUser", RPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }] }], { errorHandler: (socket, e, rpcName, args) => { if (e.message === errtxt && rpcName === "createUser" && args[0]['a'] === 'a' && args[0]['b'] === 'b') done() } }) server.listen(21004) 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(new Error("UNEXPECTED CLIENT ERROR " + e.message)) }) .finally(() => { cli.close() sock.close() server.close() }) }) }) it("handler sees sesame", (done) => { let sesame = "AAAAAAAAAAAAAAA" let server = new RPCServer([{ name: "createUser" as "createUser", RPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }] }], { sesame: sesame, errorHandler: (socket, e, rpcName, args) => { if (e.message === errtxt && rpcName === "createUser" && args[0] === sesame && args[1]['a'] === 'a' && args[1]['b'] === 'b') done() } }) server.listen(21004) 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.close() sock.close() server.close() }) }) }) }) type myExporterIfc = { MyExporter: { myRPC: () => Promise } } describe("Class binding", () => { let exporter1: MyExporter let serv: RPCServer let sock: RPCSocket & myExporterIfc let allowed = true class MyExporter implements RPCExporter{ name = "MyExporter" as "MyExporter" RPCs = () => [ this.myRPC ] myRPC = async () => { //serv.setExporters([new MyOtherExporter]) return "Hello World" } } class MyOtherExporter implements RPCExporter{ name = "MyExporter" as "MyExporter" RPCs = () => [ this.myRPC ] myRPC = async () => { return "Hello Borld" } } before(done => { exporter1 = new MyExporter() serv = new RPCServer( [exporter1], { accessFilter: async (sesame, exporter) => { if (exporter.name === 'MyExporter') { if (!allowed) return false allowed = false return sesame === 'xxx'; } else { return false } }, sesame: "xxx" }) serv.listen(21004) done() }) beforeEach((done) => { const s = new RPCSocket(21004, 'localhost') s.connect("xxx").then(conn => { sock = conn done() }) }) afterEach((done) => { sock.close() done() }) after(() => { serv.close() }) /* The server-side socket will enter a 30s timeout if destroyed by a RPC. to mitigate the impact on testing time these are not run. it("binds correctly", function(done){ this.timeout(1000) sock['MyExporter'].myRPC().then((res) => { done(new Error(res)) }).catch(e => { //job will time out because of setExporters allowed = true done() }) }) it("changes exporters", (done) => { sock['MyExporter'].myRPC().then((res) => { if (res === "Hello Borld") done() else done(new Error(res)) }) }) */ it("use sesameFilter for available", (done) => { if (sock['MyExporter']) { allowed = false done() } else done(new Error("RPC supposed to be here")) }) it("use sesameFilter", (done) => { if (!sock['MyExporter']) done() else done(new Error("RPC supposed to be gone")) }) }) describe("attaching handlers before connecting", () => { it("fires error if server is unreachable", (done) => { const sock = new RPCSocket(21004, 'localhost') let errorHandleCount = 0 sock.on('error', (err) => { //attached listener fires first if (errorHandleCount != 0) { console.log("Error handler didn't fire first"); } else { errorHandleCount++ } }) sock.connect().then(_ => { console.log("Unexpected successful connect") }).catch(e => { //catch clause fires second if (errorHandleCount != 1) { console.log("catch clause didn't fire second", errorHandleCount); } else { sock.close() done() } }) }) /* * ## 1.11.0 breaking ## * * API change: Move from bsock to socketio changes underlying API for when errors are thrown. * socketio does not throw on unknown listener. This behaviour is considered more consistent with the design * goals of RPClibrary and was thus adopted * it("fires error if call is unknown", (done) => { const serv = new RPCServer(21004) const sock = new RPCSocket(21004, 'localhost') sock.on('error', (err) => { sock.close() serv.close() done() }) sock.connect().then(_ => { sock.call("unknownRPC123", "AAAAA").catch(e => { }).then(x => { console.log("X",x); }) }).catch(e => { console.log("unexpected connect catch clause"); done(e) }) }) it("demands catch on method invocation if call is unknown", (done) => { const serv = new RPCServer(21004) const sock = new RPCSocket(21004, 'localhost') sock.connect().then(_ => { sock.call("unknownRPC123", "AAAAA").catch(e => { sock.close() serv.close() done() }) }).catch(e => { console.log("unexpected connect catch clause"); done(e) }) }) */ }) describe('finally', () => { it('print open handles (Ignore `DNSCHANNEL` and `Immediate`)', () => { //log(console) }) })