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 20KB

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