You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Test.ts 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  1. import { describe, it } from "mocha";
  2. import { RPCServer, RPCSocket } from '../Index'
  3. import { RPCExporter } from "../src/Interfaces";
  4. import { ConnectedSocket } from "../src/Types";
  5. import * as log from 'why-is-node-running';
  6. import * as http from 'http';
  7. import * as express from 'express';
  8. import * as fetch from 'node-fetch';
  9. const add = (...args: number[]) => { return args.reduce((a, b) => a + b, 0) }
  10. function makeServer() {
  11. let subcallback
  12. const sv = new RPCServer([{
  13. name: 'test',
  14. RPCs: [
  15. {
  16. name: 'echo',
  17. call: async (s: string) => s,
  18. }, {
  19. name: 'simpleSubscribe',
  20. hook: async (callback) => {
  21. subcallback = callback
  22. return { topic: "test" }
  23. },
  24. onClose: (res) => { }
  25. }, {
  26. name: 'subscribe',
  27. hook: async (callback) => {
  28. subcallback = callback
  29. return { topic: "test" }
  30. },
  31. onClose: (res, rpc) => {
  32. console.log("onClose", rpc.name === 'subscribe' && res ? "OK" : "")
  33. subcallback = null
  34. },
  35. onCallback: (...args: any) => {
  36. console.log("onCallback", args[0] === "test" && args[1] === "callback" ? "OK" : "")
  37. }
  38. },
  39. add,
  40. function triggerCallback(...messages: any[]): number { return subcallback.apply({}, messages) },
  41. ]
  42. }],
  43. {
  44. connectionHandler: (socket) => {
  45. },
  46. closeHandler: (socket) => { },
  47. errorHandler: (socket, err) => { throw err }
  48. })
  49. sv.listen(21010)
  50. return sv
  51. }
  52. describe('RPCServer', () => {
  53. let client, server
  54. const echo = (x) => x
  55. before(done => {
  56. server = new RPCServer([{
  57. name: 'HelloWorldRPCGroup',
  58. RPCs: () => [
  59. echo, //named function variable
  60. function echof(x) { return x }, //named function
  61. {
  62. name: 'echoExplicit', //describing object
  63. call: async (x, y, z) => [x, y, z]
  64. }
  65. ]
  66. }])
  67. server.listen(21003)
  68. client = new RPCSocket(21003, 'localhost')
  69. done()
  70. })
  71. after(done => {
  72. client.close()
  73. server.close()
  74. done()
  75. })
  76. it('should be able to use all kinds of RPC definitions', (done) => {
  77. client.connect().then(async () => {
  78. const r0 = await client['HelloWorldRPCGroup'].echo('Hello')
  79. const r1 = await client['HelloWorldRPCGroup'].echof('World')
  80. const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R', 'P', 'C!')
  81. if (r0 === 'Hello' && r1 === 'World' && r2.join('') === 'RPC!') {
  82. done()
  83. } else {
  84. done(new Error("Bad response"))
  85. }
  86. })
  87. })
  88. it('new RPCServer() should fail on bad RPC', (done) => {
  89. try {
  90. const sv = new RPCServer([{
  91. name: 'bad',
  92. RPCs: () => [
  93. (aaa, bbb, ccc) => { return aaa + bbb + ccc }
  94. ]
  95. }])
  96. sv.listen(20001)
  97. done(new Error("Didn't fail with bad RPC"))
  98. } catch (badRPCError) {
  99. done()
  100. }
  101. })
  102. })
  103. describe('RPCServer with premade http server', () => {
  104. const echo = (x) => x
  105. const RPCs = [
  106. echo, //named function variable
  107. function echof(x) { return x }, //named function
  108. {
  109. name: 'echoExplicit', //describing object
  110. call: async (x, y, z) => [x, y, z]
  111. }
  112. ]
  113. const RPCExporters = [
  114. {
  115. name: 'HelloWorldRPCGroup',
  116. RPCs: RPCs,
  117. }
  118. ]
  119. const RPCExporters2 = [
  120. {
  121. name: 'Grp2',
  122. RPCs: [
  123. function test(){ return "test" }
  124. ],
  125. }
  126. ]
  127. let client:RPCSocket, server:RPCServer
  128. before(done => {
  129. const expressServer = express()
  130. const httpServer = new http.Server(expressServer)
  131. expressServer.get('/REST_ping', (req, res)=>{
  132. return res
  133. .send('REST_pong')
  134. .status(200)
  135. })
  136. server = new RPCServer(
  137. RPCExporters,
  138. )
  139. server.attach(httpServer)
  140. httpServer.listen(8080)
  141. client = new RPCSocket(8080, 'localhost')
  142. done()
  143. })
  144. after(done => {
  145. client.close()
  146. server.close()
  147. done()
  148. })
  149. it('should serve REST', (done) => {
  150. fetch('http://localhost:8080/REST_ping').then(response => {
  151. response.text().then(text => {
  152. if(text === "REST_pong")
  153. done()
  154. else
  155. done(new Error("REST repsonse was "+text))
  156. })
  157. })
  158. })
  159. it('should be able to use all kinds of RPC definitions', (done) => {
  160. client.connect().then(async () => {
  161. const r0 = await client['HelloWorldRPCGroup'].echo('Hello')
  162. const r1 = await client['HelloWorldRPCGroup'].echof('World')
  163. const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R', 'P', 'C!')
  164. if (r0 === 'Hello' && r1 === 'World' && r2.join('') === 'RPC!') {
  165. done()
  166. } else {
  167. done(new Error("Bad response"))
  168. }
  169. })
  170. })
  171. })
  172. describe('RPCSocket', () => {
  173. let client: RPCSocket
  174. let server: RPCServer
  175. before(async () => {
  176. server = makeServer()
  177. client = new RPCSocket(21010, "localhost")
  178. return await client.connect()
  179. })
  180. after(() => {
  181. client.close()
  182. server.close()
  183. })
  184. it('should have rpc echo', (done) => {
  185. client['test'].echo("x").then(x => {
  186. if (x === 'x')
  187. done()
  188. else
  189. done(new Error('echo RPC response did not match'))
  190. })
  191. })
  192. it('should add up to 6', (done) => {
  193. client['test'].add(1, 2, 3).then(x => {
  194. if (x === 6)
  195. done()
  196. else
  197. done(new Error('add RPC response did not match'))
  198. })
  199. })
  200. it('should subscribe with success', (done) => {
  201. client['test'].simpleSubscribe(console.log).then(res => {
  202. if (res.topic === 'test') {
  203. done()
  204. } else {
  205. console.error(res)
  206. done(new Error('Subscribe did not return success'))
  207. }
  208. })
  209. })
  210. it('subscribe should call back', (done) => {
  211. client['test'].subscribe((...args: any) => {
  212. if (args[0] === "test" && args[1] === "callback")
  213. done()
  214. else
  215. done(new Error("Bad callback value " + args))
  216. }).then(async () => {
  217. await client['test'].triggerCallback("test", "callback")
  218. })
  219. })
  220. it('simpleSubscribe should call back', (done) => {
  221. client['test'].simpleSubscribe((...args: any) => {
  222. if (args[0] === "test_" && args[1] === "callback_")
  223. done()
  224. else
  225. done(new Error("Bad callback value " + args))
  226. }).then(async () => {
  227. await client['test'].triggerCallback("test_", "callback_")
  228. })
  229. })
  230. })
  231. describe('It should do unhook', () => {
  232. const yesCandy = "OK"
  233. const noCandy = "stolen"
  234. let candy = yesCandy
  235. let cb: Function
  236. let cb2: Function
  237. let client: RPCSocket
  238. let server: RPCServer
  239. before(async () => {
  240. server = new RPCServer([{
  241. name: "test",
  242. RPCs: () => [{
  243. name: 'subscribe',
  244. hook: async (callback): Promise<void> => {
  245. cb = <Function>callback
  246. return
  247. }
  248. },
  249. {
  250. name: 'subscribeWithParam',
  251. hook: async (param, callback): Promise<{ uuid: string }> => {
  252. if (param != "OK") {
  253. console.log("param was" + param);
  254. return {
  255. uuid: "no",
  256. }
  257. }
  258. cb2 = <Function>callback
  259. return {
  260. uuid: "OK",
  261. }
  262. }
  263. },
  264. function publish(): string { cb(candy); return candy },
  265. function unsubscribe(): string { candy = noCandy; cb(candy); cb = () => { }; return candy }
  266. ]
  267. }],
  268. {
  269. connectionHandler: (socket) => { },
  270. closeHandler: (socket) => { },
  271. errorHandler: (socket, err) => { throw err }
  272. })
  273. server.listen(21010)
  274. client = new RPCSocket(21010, "localhost")
  275. return await client.connect()
  276. })
  277. after(() => {
  278. client.close()
  279. server.close()
  280. })
  281. it('Subscribe with param', (done) => {
  282. client['test'].subscribeWithParam("OK", c => { }).then(async (res) => {
  283. if (res.uuid === candy) {
  284. done()
  285. } else
  286. done(new Error("Results did not match " + res.uuid))
  287. })
  288. })
  289. let run = 0
  290. const expected = [yesCandy, noCandy, noCandy, noCandy]
  291. it('Unhook+unsubscribe should stop callbacks', (done) => {
  292. client['test'].subscribe(function myCallback(c) {
  293. if (run == 1)
  294. (myCallback as any).destroy()
  295. if (c !== expected[run++]) {
  296. done(new Error(`Wrong candy '${c}' in iteration '${run - 1}'`))
  297. }
  298. }).then(async function (res) {
  299. const r1 = await client['test'].publish()
  300. const r3 = await client['test'].unsubscribe()
  301. const r2 = await client['test'].publish()
  302. const r4 = await client['test'].publish()
  303. if (r1 === yesCandy && r3 === noCandy && r2 === noCandy && r4 === noCandy)
  304. done()
  305. else
  306. done(new Error("Results did not match: " + [r1, r2, r3, r4]))
  307. })
  308. })
  309. })
  310. type topicDTO = { topic: string; }
  311. type SesameTestIfc = {
  312. test: {
  313. checkCandy: () => Promise<string>
  314. subscribe: (callback: Function) => Promise<topicDTO>
  315. manyParams: <A = string, B = number, C = boolean, D = Object>(a: A, b: B, c: C, d: D) => Promise<[A, B, C, D]>
  316. }
  317. other: {
  318. echo: (x: any) => Promise<any>
  319. }
  320. }
  321. describe('Sesame should unlock the socket', () => {
  322. let candy = "OK"
  323. let client: ConnectedSocket<SesameTestIfc>
  324. let server: RPCServer<SesameTestIfc>
  325. let cb: Function = (...args) => { }
  326. before((done) => {
  327. server = new RPCServer<SesameTestIfc>([{
  328. name: "test",
  329. RPCs: () => [
  330. {
  331. name: 'subscribe',
  332. hook: async (callback) => {
  333. cb = callback
  334. return {
  335. topic: 'test'
  336. }
  337. },
  338. onClose: (a) => { }
  339. },
  340. async function checkCandy() { cb(candy); cb = () => { }; return candy },
  341. async function manyParams(a, b, c, d) { return [a, b, c, d] }
  342. ],
  343. }, {
  344. name: 'other',
  345. RPCs: () => [
  346. async function echo(x) { return x }
  347. ]
  348. }], {
  349. sesame: (_sesame) => _sesame === 'sesame!'
  350. })
  351. server.listen(21004)
  352. const sock = new RPCSocket<SesameTestIfc>(21004, "localhost")
  353. sock.connect('sesame!').then(cli => {
  354. client = cli
  355. done()
  356. })
  357. })
  358. after(() => {
  359. client.close()
  360. server.close()
  361. })
  362. it('should work with sesame', (done) => {
  363. client.test.checkCandy().then(c => done())
  364. })
  365. it('should work with multiple params', (done) => {
  366. client.test['manyParams']('a', 'b', 'c', 'd').then(c => {
  367. if (c[0] == 'a' && c[1] === 'b' && c[2] === 'c' && c[3] === 'd')
  368. done()
  369. })
  370. })
  371. it('should not work without sesame', (done) => {
  372. const sock = new RPCSocket(21004, "localhost")
  373. sock.connect( /* no sesame */).then(async (cli) => {
  374. if (!cli.test)
  375. done()
  376. else {
  377. done(new Error("Function supposed to be removed without sesame"))
  378. }
  379. cli.close()
  380. sock.close()
  381. })
  382. })
  383. it('should fail with wrong sesame', (done) => {
  384. const sock = new RPCSocket(21004, "localhost")
  385. sock.connect('abasd').then(async (cli) => {
  386. if (!cli.test)
  387. done()
  388. else {
  389. done(new Error("Function supposed to be removed without sesame"))
  390. }
  391. cli.close()
  392. sock.close()
  393. })
  394. })
  395. it('callback should work with sesame', (done) => {
  396. client.test.subscribe((c) => {
  397. if (c === candy) {
  398. done()
  399. }
  400. }).then(d => {
  401. if (d.topic !== 'test')
  402. done('unexpected invalid response')
  403. client.test.checkCandy()
  404. })
  405. })
  406. })
  407. describe('Error handling', () => {
  408. const errtxt = "BAD BAD BAD"
  409. let createUser = async (user: { a: any, b: any }) => {
  410. throw new Error(errtxt)
  411. }
  412. it("RPC throws on client without handler", (done) => {
  413. let server = new RPCServer([{
  414. name: "createUser",
  415. RPCs: () => [{
  416. name: 'createUser' as 'createUser',
  417. call: createUser
  418. }]
  419. }])
  420. server.listen(21004)
  421. let sock = new RPCSocket(21004, 'localhost')
  422. sock.connect().then((cli) => {
  423. cli["createUser"]["createUser"]({
  424. a: 'a',
  425. b: 'b'
  426. })
  427. .then(r => {
  428. if (r != null)
  429. done(new Error("UNEXPECTED RESULT " + r))
  430. })
  431. .catch((e) => {
  432. if (e.message === errtxt)
  433. done()
  434. else
  435. done(e)
  436. })
  437. .finally(() => {
  438. cli.close()
  439. sock.close()
  440. server.close()
  441. })
  442. })
  443. })
  444. it("RPC throws on server with handler", (done) => {
  445. let server = new RPCServer([{
  446. name: "createUser",
  447. RPCs: () => [{
  448. name: 'createUser' as 'createUser',
  449. call: createUser
  450. }]
  451. }], {
  452. errorHandler: (socket, e, rpcName, args) => {
  453. done()
  454. }
  455. })
  456. server.listen(21004)
  457. let sock = new RPCSocket(21004, 'localhost')
  458. sock.connect().then((cli) => {
  459. cli["createUser"]["createUser"]({
  460. a: 'a',
  461. b: 'b'
  462. })
  463. .then(r => {
  464. if (r != null)
  465. done("UNEXPECTED RESULT " + r)
  466. })
  467. .catch((e) => {
  468. done("UNEXPECTED CLIENT ERROR " + e)
  469. done(e)
  470. })
  471. .finally(() => {
  472. cli.close()
  473. sock.close()
  474. server.close()
  475. })
  476. })
  477. })
  478. })
  479. describe("Errorhandler functionality", () => {
  480. const errtxt = "BAD BAD BAD"
  481. let createUser = async (user: { a: any, b: any }) => {
  482. throw new Error(errtxt)
  483. }
  484. it("correct values are passed to the handler", (done) => {
  485. let server = new RPCServer([{
  486. name: "createUser",
  487. RPCs: () => [{
  488. name: 'createUser' as 'createUser',
  489. call: createUser
  490. }]
  491. }], {
  492. errorHandler: (socket, e, rpcName, args) => {
  493. if (e.message === errtxt && rpcName === "createUser" && args[0]['a'] === 'a' && args[0]['b'] === 'b')
  494. done()
  495. }
  496. })
  497. server.listen(21004)
  498. let sock = new RPCSocket(21004, 'localhost')
  499. sock.connect().then((cli) => {
  500. cli["createUser"]["createUser"]({
  501. a: 'a',
  502. b: 'b'
  503. })
  504. .then(r => {
  505. if (r != null)
  506. done("UNEXPECTED RESULT " + r)
  507. })
  508. .catch((e) => {
  509. done(new Error("UNEXPECTED CLIENT ERROR " + e.message))
  510. })
  511. .finally(() => {
  512. cli.close()
  513. sock.close()
  514. server.close()
  515. })
  516. })
  517. })
  518. it("handler sees sesame", (done) => {
  519. let sesame = "AAAAAAAAAAAAAAA"
  520. let server = new RPCServer([{
  521. name: "createUser" as "createUser",
  522. RPCs: () => [{
  523. name: 'createUser' as 'createUser',
  524. call: createUser
  525. }]
  526. }], {
  527. sesame: sesame,
  528. errorHandler: (socket, e, rpcName, args) => {
  529. if (e.message === errtxt && rpcName === "createUser" && args[0] === sesame && args[1]['a'] === 'a' && args[1]['b'] === 'b')
  530. done()
  531. }
  532. })
  533. server.listen(21004)
  534. let sock = new RPCSocket(21004, 'localhost')
  535. sock.connect(sesame).then((cli) => {
  536. cli["createUser"]["createUser"]({
  537. a: 'a',
  538. b: 'b'
  539. })
  540. .then(r => {
  541. if (r != null)
  542. done("UNEXPECTED RESULT " + r)
  543. })
  544. .catch((e) => {
  545. done("UNEXPECTED CLIENT ERROR " + e)
  546. done(e)
  547. })
  548. .finally(() => {
  549. cli.close()
  550. sock.close()
  551. server.close()
  552. })
  553. })
  554. })
  555. })
  556. type myExporterIfc = {
  557. MyExporter: {
  558. myRPC: () => Promise<string>
  559. }
  560. }
  561. describe("Class binding", () => {
  562. let exporter1: MyExporter
  563. let serv: RPCServer<myExporterIfc>
  564. let sock: RPCSocket & myExporterIfc
  565. let allowed = true
  566. class MyExporter implements RPCExporter<myExporterIfc>{
  567. name = "MyExporter" as "MyExporter"
  568. RPCs = () => [
  569. this.myRPC
  570. ]
  571. myRPC = async () => {
  572. //serv.setExporters([new MyOtherExporter])
  573. return "Hello World"
  574. }
  575. }
  576. class MyOtherExporter implements RPCExporter<myExporterIfc>{
  577. name = "MyExporter" as "MyExporter"
  578. RPCs = () => [
  579. this.myRPC
  580. ]
  581. myRPC = async () => {
  582. return "Hello Borld"
  583. }
  584. }
  585. before(done => {
  586. exporter1 = new MyExporter()
  587. serv = new RPCServer<myExporterIfc>(
  588. [exporter1],
  589. {
  590. accessFilter: async (sesame, exporter) => {
  591. if (exporter.name === 'MyExporter') {
  592. if (!allowed) return false
  593. allowed = false
  594. return sesame === 'xxx';
  595. } else {
  596. return false
  597. }
  598. },
  599. sesame: "xxx"
  600. })
  601. serv.listen(21004)
  602. done()
  603. })
  604. beforeEach((done) => {
  605. const s = new RPCSocket<myExporterIfc>(21004, 'localhost')
  606. s.connect("xxx").then(conn => {
  607. sock = conn
  608. done()
  609. })
  610. })
  611. afterEach((done) => {
  612. sock.close()
  613. done()
  614. })
  615. after(() => {
  616. serv.close()
  617. })
  618. /* The server-side socket will enter a 30s timeout if destroyed by a RPC.
  619. to mitigate the impact on testing time these are not run.
  620. it("binds correctly", function(done){
  621. this.timeout(1000)
  622. sock['MyExporter'].myRPC().then((res) => {
  623. done(new Error(res))
  624. }).catch(e => {
  625. //job will time out because of setExporters
  626. allowed = true
  627. done()
  628. })
  629. })
  630. it("changes exporters", (done) => {
  631. sock['MyExporter'].myRPC().then((res) => {
  632. if (res === "Hello Borld")
  633. done()
  634. else
  635. done(new Error(res))
  636. })
  637. })
  638. */
  639. it("use sesameFilter for available", (done) => {
  640. if (sock['MyExporter']) {
  641. allowed = false
  642. done()
  643. }
  644. else done(new Error("RPC supposed to be here"))
  645. })
  646. it("use sesameFilter", (done) => {
  647. if (!sock['MyExporter']) done()
  648. else done(new Error("RPC supposed to be gone"))
  649. })
  650. })
  651. describe("attaching handlers before connecting", () => {
  652. it("fires error if server is unreachable", (done) => {
  653. const sock = new RPCSocket<myExporterIfc>(21004, 'localhost')
  654. let errorHandleCount = 0
  655. sock.on('error', (err) => {
  656. //attached listener fires first
  657. if (errorHandleCount != 0) {
  658. console.log("Error handler didn't fire first");
  659. } else {
  660. errorHandleCount++
  661. }
  662. })
  663. sock.connect().then(_ => {
  664. console.log("Unexpected successful connect")
  665. }).catch(e => {
  666. //catch clause fires second
  667. if (errorHandleCount != 1) {
  668. console.log("catch clause didn't fire second", errorHandleCount);
  669. } else {
  670. sock.close()
  671. done()
  672. }
  673. })
  674. })
  675. /*
  676. * ## 1.11.0 breaking ##
  677. *
  678. * API change: Move from bsock to socketio changes underlying API for when errors are thrown.
  679. * socketio does not throw on unknown listener. This behaviour is considered more consistent with the design
  680. * goals of RPClibrary and was thus adopted
  681. *
  682. it("fires error if call is unknown", (done) => {
  683. const serv = new RPCServer(21004)
  684. const sock = new RPCSocket(21004, 'localhost')
  685. sock.on('error', (err) => {
  686. sock.close()
  687. serv.close()
  688. done()
  689. })
  690. sock.connect().then(_ => {
  691. sock.call("unknownRPC123", "AAAAA").catch(e => { }).then(x => {
  692. console.log("X",x);
  693. })
  694. }).catch(e => {
  695. console.log("unexpected connect catch clause");
  696. done(e)
  697. })
  698. })
  699. it("demands catch on method invocation if call is unknown", (done) => {
  700. const serv = new RPCServer(21004)
  701. const sock = new RPCSocket(21004, 'localhost')
  702. sock.connect().then(_ => {
  703. sock.call("unknownRPC123", "AAAAA").catch(e => {
  704. sock.close()
  705. serv.close()
  706. done()
  707. })
  708. }).catch(e => {
  709. console.log("unexpected connect catch clause");
  710. done(e)
  711. })
  712. })
  713. */
  714. })
  715. describe('finally', () => {
  716. it('print open handles (Ignore `DNSCHANNEL` and `Immediate`)', () => {
  717. //log(console)
  718. })
  719. })