import { describe, it } from "mocha"; import { RPCServer, RPCSocket, Serializable } from '../Index' import { RPCExporter, Socket } from "../src/Interfaces"; import { ConnectedSocket, Callback, GenericFunction } 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'; import { PromiseIO } from "../src/PromiseIO/Server"; import { PromiseIOClient } from "../src/PromiseIO/Client"; import { assert, expect } from 'chai'; import { CLASSNAME_ATTRIBUTE, USER_DEFINED_TIMEOUT } from "../src/Strings"; var should = require('chai').should(); var chai = require("chai"); var chaiAsPromised = require("chai-as-promised"); chai.use(chaiAsPromised); const noop = (...args) => { } const add = (...args: number[]) => { return args.reduce((a, b) => a + b, 0) } function makeServer(onCallback = noop, connectionHandler = noop, hookCloseHandler = noop, closeHandler = noop, errorHandler = noop) { let subcallback const serv = new RPCServer([{ name: 'test', RPCs: [ { name: 'echo', call: async (s: string) => s, }, { name: 'complexSignature', call: async ([a, b]) => { return [b, a] } }, { name: 'simpleSubscribe', hook: async (callback) => { subcallback = callback return { topic: "test" } }, onDestroy: hookCloseHandler }, { name: 'subscribe', hook: async (callback) => { subcallback = callback return { topic: "test" } }, onDestroy: hookCloseHandler, onCallback: onCallback }, add, function triggerCallback(...messages: any[]): number { return subcallback.apply({}, messages) }, function brokenRPC() { throw new Error("Intended error") } ] }], { connectionHandler: connectionHandler, closeHandler: closeHandler, errorHandler: errorHandler }) serv.listen(21010) return serv } describe('PromiseIO', () => { it("bind + fire", (done) => { const server = new PromiseIO() server.attach(new http.Server()) server.on("socket", clientSocket => { clientSocket.bind("test123", (p1, p2) => { server.close() if (p1 === "p1" && p2 === "p2") done() }) }); server.listen(21003) PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => { cli.fire("test123", "p1", "p2") cli.close() }) }) it("hook + call", (done) => { const server = new PromiseIO() server.attach(new http.Server()) server.on("socket", clientSocket => { clientSocket.hook("test123", (p1, p2) => { if (p1 === "p1" && p2 === "p2") return "OK" }) }); server.listen(21003) PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => { cli.call("test123", "p1", "p2").then(resp => { cli.close() server.close() if (resp === "OK") done() }) }) }) it("on + emit", (done) => { const server = new PromiseIO() server.attach(new http.Server()) server.on("socket", clientSocket => { clientSocket.on("test123", (p1, p2) => { server.close() if (p1 === "p1" && p2 === "p2") done() }) }); server.listen(21003) PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => { cli.emit("test123", "p1", "p2") cli.close() }) }) }) 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', async () => { await client.connect() const r0 = await client['HelloWorldRPCGroup'].echo('Hello') const r1 = await client['HelloWorldRPCGroup'].echof('World') const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R', 'P', 'C!') expect(r0).to.be.equal('Hello') expect(r1).to.be.equal('World') expect(r2.join('')).to.be.equal('RPC!') }) it('new RPCServer() should fail on unnamed RPC', async () => { expect(() => { const sv = new RPCServer([{ name: 'bad', RPCs: () => [ (aaa, bbb, ccc) => { return aaa + bbb + ccc } ] }]) }).to.throw() }) }) 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', async () => { const response = await fetch('http://localhost:8080/REST_ping') const text = await response.text() expect(text).to.be.equal("REST_pong") }) it('should be able to use all kinds of RPC definitions', async () => { await client.connect() const r0 = await client['HelloWorldRPCGroup'].echo('Hello') const r1 = await client['HelloWorldRPCGroup'].echof('World') const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R', 'P', 'C!') expect(r0).to.be.equal('Hello') expect(r1).to.be.equal('World') expect(r2.join('')).to.be.equal('RPC!') }) }) describe('should be able to attach to non-standard path', () => { let client: RPCSocket, server: RPCServer 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, { path: '/test' }) client = new RPCSocket(21003, 'localhost', { path: '/test' }) done() }) after(done => { client.close() server.close() done() }) it('should be able to use all kinds of RPC definitions', async () => { await client.connect() const r0 = await client['HelloWorldRPCGroup'].echo('Hello') const r1 = await client['HelloWorldRPCGroup'].echof('World') const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R', 'P', 'C!') expect(r0).to.be.equal('Hello') expect(r1).to.be.equal('World') expect(r2.join('')).to.be.equal('RPC!') }) }) describe('can attach multiple RPCServers to same 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" } ], } ] const callTimeout = 100; let client: RPCSocket, client2: RPCSocket, server: RPCServer, server2: RPCServer before(done => { const expressServer = express() const httpServer = new http.Server(expressServer) server = new RPCServer( RPCExporters, ) server2 = new RPCServer( RPCExporters2 ) server.attach(httpServer) server2.attach(httpServer, { path: "test" }) httpServer.listen(8080) new RPCSocket(8080, 'localhost').connect().then(sock => { client = sock new RPCSocket(8080, 'localhost', { path: "test", callTimeoutMs: callTimeout }).connect().then(sock2 => { client2 = sock2 done() }) }) }) after(done => { client.close() client2.close() server.close() server2.close() done() }) it('both servers should answer', async () => { const res = await client['HelloWorldRPCGroup'].echo("test") expect(res).to.equal('test') const res2 = await client2['Grp2'].test() expect(res2).to.equal('/test') }) it('server1 should answer after server2 is closed', async () => { server2.close() const res = await client['HelloWorldRPCGroup'].echo("test") expect(res).to.equal('test') return client2['Grp2'].test().should.eventually.be.rejectedWith(USER_DEFINED_TIMEOUT(callTimeout)) }) }) describe("can attach second RPCServer if first is already running", () => { const RPCExporters = [ { name: 'HelloWorldRPCGroup', RPCs: [ function echo(x) { return x }, //named function variable function echof(x) { return x }, //named function { name: 'echoExplicit', //describing object call: async (x, y, z) => [x, y, z] } ], } ] const RPCExporters2 = [ { name: 'Grp2', RPCs: [ function test() { return "/test" } ], } ] it("attaches correctly", async () => { const expressServer = express() const httpServer = new http.Server(expressServer) const server = new RPCServer( RPCExporters, ) const server2 = new RPCServer( RPCExporters2 ) server.attach(httpServer) httpServer.listen(8080) server2.attach(httpServer, { path: "test" }) const sock = await new RPCSocket(8080, 'localhost').connect() const sock2 = await new RPCSocket(8080, 'localhost', { path: "test" }).connect() const resp = await sock2.Grp2.test() expect(resp).to.be.equal("/test") server.close() server2.close() sock.close() sock2.close() }) }) describe('Serverside Triggers', () => { let server, client const closerFunction = (done) => () => { client.close() server.close() done() } it('trigger onCallback', (done) => { server = makeServer(closerFunction(done)) client = new RPCSocket(21010, "localhost") client.connect().then(_ => { client['test'].subscribe(noop).then(_ => client['test'].triggerCallback()) }) }) /* testing framework has trouble terminating on this one it('trigger connectionHandler', (done) => { server = makeServer(undefined, closerFunction(done)) client = new RPCSocket(21010, "localhost") client.connect() }) */ it('trigger hook closeHandler', (done) => { server = makeServer(undefined, undefined, closerFunction(done)) client = new RPCSocket(21010, "localhost") client.connect().then(_ => { client['test'].subscribe(function cb() { cb['destroy']() }).then(_ => client['test'].triggerCallback()) }) }) it('trigger global closeHandler', (done) => { server = makeServer(undefined, undefined, undefined, () => { server.close() done() }) client = new RPCSocket(21010, "localhost") client.connect().then(_ => { client['test'].subscribe(noop).then(_ => client.close()) }) }) }) 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', async () => { const x = await client['test'].echo("x") expect(x).to.be.equal('x') }) it('should add up to 6', async () => { const sum = await client['test'].add(1, 2, 3) expect(sum).to.be.equal(6) }) it('should subscribe with success', async () => { const res = await client['test'].simpleSubscribe(noop) expect(res.topic).to.be.equal('test') }) 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: noop, closeHandler: noop, 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', async () => { const res = await client['test'].subscribeWithParam("OK", noop) expect(res.uuid).to.be.equal(candy) }) let run = 0 const expected = [yesCandy, noCandy, noCandy, noCandy] it('Unhook+unsubscribe should stop callbacks', async () => { await client['test'].subscribe(function myCallback(c) { if (run == 1) (myCallback as any).destroy() expect(c).to.equal(expected[run++]) }) const r1 = await client['test'].publish() const r3 = await client['test'].unsubscribe() const r2 = await client['test'].publish() const r4 = await client['test'].publish() expect(r1).to.be.equal(yesCandy) expect(r2).to.be.equal(noCandy) expect(r3).to.be.equal(noCandy) expect(r4).to.be.equal(noCandy) }) }) type topicDTO = { topic: string; } type SesameTestIfc = { test: { checkCandy: () => Promise subscribe: (callback: Callback<[string]>) => Promise manyParams: (a: A, b: B, c: C, d: D) => Promise<[A, B, C, D]> } } 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' } }, onDestroy: noop }, async function checkCandy() { cb(candy); cb = noop; return candy }, async function manyParams(a, b, c, d) { return [a, b, c, d] } ], }], { 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', async () => { const c = client.test.checkCandy() expect(c).to.exist }) it('should work with multiple params', async () => { const c = await client.test['manyParams']('a', 'b', 'c', 'd') expect(c[0]).to.be.equal('a') expect(c[1]).to.be.equal('b') expect(c[2]).to.be.equal('c') expect(c[3]).to.be.equal('d') }) it('should not work without sesame', async () => { const sock = new RPCSocket(21004, "localhost") const cli = await sock.connect() expect(cli.test).to.not.exist cli.close() sock.close() }) it('should fail with wrong sesame', async () => { const sock = new RPCSocket(21004, "localhost") const cli = await sock.connect('iamwrong') expect(cli.test).to.not.exist cli.close() sock.close() }) it('callback should work with sesame', (done) => { client.test.subscribe(function (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(new Error("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(new Error("UNEXPECTED RESULT " + r)) }) .catch((e) => { done(new Error("UNEXPECTED CLIENT ERROR " + 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 const SESAME = 'xyz' 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 === SESAME; } else { return false } }, sesame: SESAME }) serv.listen(21004) done() }) beforeEach((done) => { const s = new RPCSocket(21004, 'localhost') s.connect(SESAME).then(conn => { sock = conn done() }) }) afterEach((done) => { sock.close() done() }) after(() => { serv.close() }) 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('finally', () => { it('print open handles (Ignore `DNSCHANNEL` and `Immediate`)', () => { //log(console) }) }) */ 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() } }) }) it("fires error if call is unknown", (done) => { const serv = new RPCServer().listen(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) }) }) }) describe("class (de-)serialization", () => { @Serializable() class SubClass { fString = "F" } @Serializable() class TestClass { aString = "A" aNumber = 46 aObject = { x: "x", y: undefined, sub: new SubClass() } aClassObject = new SubClass() public returnOK() { return "OK" } } const verifyObject = (obj: any) => { expect(obj).to.be.an.instanceOf(TestClass) expect(obj.aString).to.be.a('string') expect(obj.aNumber).to.be.a('number') expect(obj.aObject).to.be.a('object') expect(obj.aObject.x).to.be.a('string') expect(obj.aObject.y).to.be.undefined expect(obj.aObject.sub).to.be.an.instanceOf(SubClass) expect(obj.aClassObject).to.be.an.instanceOf(SubClass) expect(obj).to.not.have.key(CLASSNAME_ATTRIBUTE) expect(obj.aObject.sub).to.not.have.key(CLASSNAME_ATTRIBUTE) expect(obj.aClassObject).to.not.have.key(CLASSNAME_ATTRIBUTE) expect(obj.returnOK()).to.be.equal('OK') } describe("Responses", () => { type TestIfc = { Test: { returnClass: () => Promise classCallback: (callback: Callback<[TestClass]>) => Promise } } let myServer: RPCServer; let mySocket: ConnectedSocket; before(function (done) { myServer = new RPCServer([{ name: "Test", RPCs: [ async function returnClass() { return new TestClass() }, { name: "classCallback", hook: async function (callback) { setTimeout(_ => callback(new TestClass()), 250) return new TestClass() } } ] }]) myServer.listen(8084) new RPCSocket(8084, 'localhost').connect().then(connsock => { mySocket = connsock done() }) }) after(function (done) { mySocket.close() myServer.close() done() }) it("receives class object in call response", async () => { const obj: TestClass = await mySocket['Test'].returnClass() verifyObject(obj) }) it("receives class object in hook response", async function () { const obj: TestClass = await mySocket.Test.classCallback(noop) verifyObject(obj) }) it("receives class object in callback", function (done) { mySocket.Test.classCallback(function (cbValue) { verifyObject(cbValue) done() }).then(verifyObject) }) }) describe("Parameters", () => { it("Class object in call", function(done){ const server = new RPCServer([ { name: "Test", RPCs: [ function callWithClass(testObj: TestClass){ verifyObject(testObj) done() } ] } ]).listen(8086) new RPCSocket(8086, 'localhost').connect().then(sock => { sock['Test'].callWithClass(new TestClass()).then(_ => { sock.close() server.close() }) }) }) }) })