UNPKG

rpcchannel

Version:

Easy RPC with permission controls

1,376 lines (1,343 loc) 45.2 kB
import { expect } from 'chai' import { toRpcSerialized, InvalidChannelError, ForwardedError, RpcChannel, RpcMessage, RpcRemappedFunction, RpcAddress, RemapArguments, RpcFunctionAddress, RpcFunction, RpcHandlerRegistry, MultistringAddress, FunctionAccessController, RpcState } from '../src/index' /** * This library was designed for full async, so sometimes, it is impossible to * `await` a specific promise completion to see if a generator has created the * correct messages, for example. The easiest solution was to literally just * wait a while to ensure that the `expect`ing code runs at the bottom of the * async queue. */ const static_await_delay = 1 describe('[registry.ts] RpcAddress', () => { it('sets RpcFunctionAddress on member function', () => { class Test { @RpcAddress(['net', 'kb1rd', undefined]) member(): number { return 1 } } expect((new Test().member as any)[RpcFunctionAddress]).to.be.deep.equal( ['net', 'kb1rd', undefined] ) }) it('throws error when applied to non-function', () => { expect(() => { const myobj = {} RpcAddress([])( myobj, 'test', { configurable: true, enumerable: false, value: 'hi', writable: true, get(): string { return 'hi' }, set(v: any): void { throw new Error('Should not happen') } } ) }).to.throw('Cannot mark non-function as RPC function') }) }) describe('[registry.ts] RpcHandlerRegistry', () => { it('nextSeqAddr allocates sequential return addresses', () => { const hr = new RpcHandlerRegistry() expect(hr.nextSeqAddr()).to.be.deep.equal(['_', 'ret', 'id0']) expect(hr.nextSeqAddr()).to.be.deep.equal(['_', 'ret', 'id1']) expect(hr.nextSeqAddr()).to.be.deep.equal(['_', 'ret', 'id2']) expect(hr.nextSeqAddr()).to.be.deep.equal(['_', 'ret', 'id3']) }) }) describe('[registry.ts] RpcChannel', () => { // Register already tested via AddressMap. I know, UNIT testing, but I'm lazy let c: RpcChannel let sent_msgs: [RpcMessage, Transferable[]][] beforeEach(() => { sent_msgs = [] c = new RpcChannel((msg, xfer) => sent_msgs.push([msg, xfer])) }) afterEach(() => { c.close() }) describe('_stateChange', () => { it('emits event', () => { let events: [RpcState, RpcState][] = [] c.on('statechange', (c, o) => events.push([c, o])) c._stateChange(RpcState.ACTIVE) expect(events).to.be.deep.equal([[RpcState.ACTIVE, RpcState.INACTIVE]]) events.length = 0 c._stateChange(RpcState.CLOSED) expect(events).to.be.deep.equal([[RpcState.CLOSED, RpcState.ACTIVE]]) }) it('emits active event', () => { let events: undefined[] = [] c.on('active', () => events.push(undefined)) c._stateChange(RpcState.ACTIVE) expect(events.length).to.be.deep.equal(1) c._stateChange(RpcState.CLOSED) expect(events.length).to.be.deep.equal(1) }) it('emits closed event', () => { let events: undefined[] = [] c.on('close', () => events.push(undefined)) c._stateChange(RpcState.CLOSED) expect(events.length).to.be.deep.equal(1) c._stateChange(RpcState.ACTIVE) expect(events.length).to.be.deep.equal(1) }) }) describe('start/stop', () => { it('starts `INACTIVE`', () => { expect(c.state).to.be.equal(RpcState.INACTIVE) }) it('`start` transitions to `ACTIVE`', async () => { expect(c.state).to.be.equal(RpcState.INACTIVE) await c.start() expect(c.state).to.be.equal(RpcState.ACTIVE) }) it('waits for message if `await_first_msg` is set', async () => { expect(c.state).to.be.equal(RpcState.INACTIVE) c.setAwaitFirstMsg(true) let done = false c.start().then(() => (done = true)) expect(done).to.be.false await new Promise((r) => setTimeout(r, 20)) expect(done).to.be.false expect(c.state).to.be.equal(RpcState.INACTIVE) c.receive({ to: ['_', 'keepalive'], args: [] }) await new Promise((r) => setTimeout(r, 1)) expect(done).to.be.true expect(c.state).to.be.equal(RpcState.ACTIVE) }) it('starts timeout', async () => { c.setTimeout(5000) expect(c.active_timeout).to.be.undefined await c.start() expect(c.active_timeout).to.not.be.undefined }) it('starts timeout only after awaiting msg', async () => { c.setTimeout(5000) c.setAwaitFirstMsg(true) expect(c.active_timeout).to.be.undefined const promise = c.start() await new Promise((r) => setTimeout(r, 1)) expect(c.active_timeout).to.be.undefined c.receive({ to: ['_', 'keepalive'], args: [] }) await promise expect(c.active_timeout).to.not.be.undefined }) it('stays closed if closed during await', async () => { expect(c.state).to.be.equal(RpcState.INACTIVE) c.setAwaitFirstMsg(true) // Note the *lack* of await let done = false c.start().then(() => (done = true)) await new Promise((r) => setTimeout(r, 5)) expect(done).to.be.false c.close() await new Promise((r) => setTimeout(r, 1)) expect(done).to.be.true expect(c.state).to.be.equal(RpcState.CLOSED) c.receive({ to: ['_', 'keepalive'], args: [] }) await new Promise((r) => setTimeout(r, 5)) expect(c.state).to.be.equal(RpcState.CLOSED) }) it('stays closed if closed before start', async () => { expect(c.state).to.be.equal(RpcState.INACTIVE) c.close() expect(c.state).to.be.equal(RpcState.CLOSED) await c.start() expect(c.state).to.be.equal(RpcState.CLOSED) }) }) describe('timeouts', () => { it('closes channel after time elapsed', async () => { c.setTimeout(20) await c.start() expect(c.state).to.be.equal(RpcState.ACTIVE) await new Promise((r) => setTimeout(r, 1)) expect(c.state).to.be.equal(RpcState.ACTIVE) await new Promise((r) => setTimeout(r, 20)) expect(c.state).to.be.equal(RpcState.CLOSED) }) it('does not close channel if any event sent', async () => { c.setTimeout(20) await c.start() expect(c.state).to.be.equal(RpcState.ACTIVE) await new Promise((r) => setTimeout(r, 10)) expect(c.state).to.be.equal(RpcState.ACTIVE) c.receive({ to: ['sadfsd', 'fsdfsd', 'sdfds'], args: [] }) await new Promise((r) => setTimeout(r, 11)) expect(c.state).to.be.equal(RpcState.ACTIVE) }) it('clears old timeout when timeout changed', async () => { c.setTimeout(20) await c.start() expect(c.state).to.be.equal(RpcState.ACTIVE) await new Promise((r) => setTimeout(r, 10)) expect(c.state).to.be.equal(RpcState.ACTIVE) c.setTimeout(15) await new Promise((r) => setTimeout(r, 10)) expect(c.state).to.be.equal(RpcState.ACTIVE) }) }) describe('keepalive', () => { it('sends initial keepalive on setup', async () => { c.setTimeout(undefined, 10) expect(sent_msgs).to.be.deep.equal([[ { to: ['_', 'keepalive'], args: [], return_type: 'promise', return_addr: undefined }, [] ]]) }) it('sends keepalive on interval', async () => { c.setTimeout(undefined, 10) await c.start() sent_msgs.length = 0 await new Promise((r) => setTimeout(r, 5)) expect(sent_msgs.length).to.be.equal(0) await new Promise((r) => setTimeout(r, 6)) expect(sent_msgs).to.be.deep.equal([[ { to: ['_', 'keepalive'], args: [], return_type: 'promise', return_addr: undefined }, [] ]]) }) }) class Test { @RpcAddress(['net', 'kb1rd', undefined]) member(): number { return 1 } @RpcAddress(['net', 'kb1rd', 'addnum', undefined]) addnum(c: RpcChannel, wc: string[], n: number): string { return wc[0] + n } @RpcAddress(['net', 'kb1rd', 'getthis']) getthis(): Test { return this } @RemapArguments(['drop'], 'rm') remapped_dropfirst(b: string): string { return b } @RemapArguments(['expand', 'expand'], 'rm') remapped_expand(a: string, b: string): string { return a + b } @RemapArguments(['expand', 'pass'], 'rm') remapped_expand_pass(a: string, b: string): string { return a + b } @RemapArguments(['pass', 'pass'], 'rm') remapped_pass(a: string, b: string): string { return a + b } @RpcAddress(['net', 'kb1rd', 'dropfirst']) @RemapArguments(['drop']) remapped_dropfirst_rpc(b: string): string { return b } } describe('RemapArguments', () => { // Note: These tests do not call the function with `apply`. Always call // with `apply` it('drops correctly', () => { const test = new Test() const res = ( test.remapped_dropfirst as unknown as { rm: (...args: any[]) => any } ).rm('hi', '123') expect(res).to.be.equal('123') }) it('expands correctly', () => { const test = new Test() const res = ( test.remapped_expand as unknown as { rm: (...args: any[]) => any } ).rm(['hi', '123']) expect(res).to.be.equal('hi123') }) it('passes correctly', () => { const test = new Test() const res = ( test.remapped_pass as unknown as { rm: (...args: any[]) => any } ).rm('hi', '123') expect(res).to.be.equal('hi123') }) it('expand does not consume next directive', () => { const test = new Test() const res = ( test.remapped_expand_pass as unknown as { rm: (...args: any[]) => any } ).rm(['hi'], '123') expect(res).to.be.equal('hi123') }) it('throws when given non-function', () => { expect(() => { RemapArguments([])( {}, 'test', { configurable: true, enumerable: false, value: 'hi', writable: true, get(): string { return 'hi' }, set(v: any): void { throw new Error('Should not happen') } } ) }).to.throw('Cannot remap arguments for non-function') }) it('throws when expanding non-iterable', () => { const test = new Test() expect(() => { ;(test.remapped_expand as unknown as { rm: (...args: any[]) => any }) .rm(123) }).to.throw('Attempted to expand non-iterable') }) it('throws when not enough elements', () => { const test = new Test() expect(() => { ;(test.remapped_expand as unknown as { rm: (...args: any[]) => any }) .rm([]) }).to.throw('Expand reached end of array') }) }) describe('register', () => { beforeEach(() => c.start()) it('basic register', () => { const func = () => undefined c.register(['net', 'kb1rd', 'test'], func) expect(c.reg.map.get(['net', 'kb1rd', 'test'])).to.be.equal(func) }) it('register from remapped function key', () => { interface RemappedFunction { (): undefined [key: string]: () => undefined } const func: RemappedFunction = (() => undefined) as RemappedFunction const func2 = () => undefined func[(RpcRemappedFunction as unknown) as string] = func2 c.register(['net', 'kb1rd', 'test'], func) expect(c.reg.map.get(['net', 'kb1rd', 'test'])).to.be.equal(func2) }) }) describe('unregister', () => { beforeEach(() => c.start()) it('unregister by address', () => { const func = () => undefined c.register(['net', 'kb1rd', 'test'], func) c.unregister(['net', 'kb1rd', 'test']) expect(c.reg.map.get(['net', 'kb1rd', 'test'])).to.be.undefined }) }) describe('registerAll', () => { beforeEach(() => c.start()) it('registers to correct endpoint', () => { c.registerAll(new Test() as {}) expect( typeof c.reg.map.get(['net', 'kb1rd', 'literally']) ).to.be.equal('function') expect( typeof c.reg.map.get(['net', 'kb1rd', 'anything']) ).to.be.equal('function') }) it('maintains correct value of `this`', () => { const test = new Test() c.registerAll(test as {}) expect( (c.reg.map.get(['net', 'kb1rd', 'getthis']) as RpcFunction)(c, []) ).to.be.equal(test) }) it('register from remapped function key', () => { const test = new Test() c.registerAll(test as {}) const a: string[] = [] expect( (c.reg.map.get(['net', 'kb1rd', 'dropfirst']) as RpcFunction)(c, a) ).to.be.equal(a) }) }) describe('unregisterAll', () => { beforeEach(() => c.start()) it('unregisters endpoints', () => { c.registerAll(new Test() as {}) c.unregisterAll(new Test() as {}) expect(c.reg.map.get(['net', 'kb1rd', 'literally'])).to.be.undefined expect(c.reg.map.get(['net', 'kb1rd', 'anything'])).to.be.undefined }) }) describe('send', () => { beforeEach(() => c.start()) it('defaults to empty args', () => { c.send(['net', 'kb1rd', 'hello']) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('serializes arguments', () => { c.send( ['net', 'kb1rd', 'hello'], [123, 'abc', { [toRpcSerialized]: () => 'hi' }] ) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [123, 'abc', 'hi'], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('adds `return_addr`', () => { c.send( ['net', 'kb1rd', 'hello'], [], ['net', 'kb1rd', 'callreturn'] ) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [], return_addr: ['net', 'kb1rd', 'callreturn'], return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) }) describe('call', () => { beforeEach(() => c.start()) it('defaults to empty args', () => { c.call(['net', 'kb1rd', 'hello']).catch(() => undefined) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [], return_addr: sent_msgs[0][0].return_addr, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('passes args to `send`', () => { c.call( ['net', 'kb1rd', 'hello'], [123, 'abc', { [toRpcSerialized]: () => 'hi' }] ).catch(() => undefined) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [123, 'abc', 'hi'], return_addr: sent_msgs[0][0].return_addr, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('generates a unique `return_addr`', () => { c.call( ['net', 'kb1rd', 'hello'], [123, 'abc', { [toRpcSerialized]: () => 'hi' }] ).catch(() => undefined) c.call( ['net', 'kb1rd', 'hello'], [123, 'abc', { [toRpcSerialized]: () => 'hi' }] ).catch(() => undefined) expect(sent_msgs.length).to.be.equal(2) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [123, 'abc', 'hi'], return_addr: ['_', 'ret', 'id0'], return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(sent_msgs[1][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [123, 'abc', 'hi'], return_addr: ['_', 'ret', 'id1'], return_type: 'promise' }) expect(sent_msgs[1][1].length).to.be.equal(0) }) it('registers handler and returns async', async () => { // Used to make sure promise is not pre-resolved let then_done: boolean = false let error: boolean = false const promise = c.call(['net', 'kb1rd', 'hello'], []).then( () => (then_done = true), () => (then_done = error = true) ) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [], return_addr: sent_msgs[0][0].return_addr, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(typeof c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress )).to.be.equal('function') expect(then_done).to.be.false ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, []) await promise expect(then_done).to.be.true expect(error).to.be.false }) it('resolves promise with return values', async () => { const promise = c.call(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], 'hello', undefined) const data = await promise expect(data).to.be.equal('hello') }) it('rejects promise with second error argument', async () => { const promise = c.call(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], undefined, 'ERROR!') let error: any try { await promise } catch(e) { error = e } expect(error).to.be.equal('ERROR!') }) it('rejects promise if callback called with other channel', async () => { const promise = c.call(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(new RpcChannel(() => undefined), []) let error: any try { await promise } catch(e) { error = e } expect(error).to.be.an.instanceOf(InvalidChannelError) }) it('rejects promise with ForwardedError if error passed', async () => { const promise = c.call(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], undefined, { name: 'ERROR!' }) let error: any try { await promise } catch(e) { error = e } expect(error).to.be.an.instanceOf(ForwardedError) expect(error.name).to.be.equal('ERROR!') }) it('rejects promise if channel closed', async () => { const promise = c.call(['net', 'kb1rd', 'hello'], []) c.close() let error: any try { await promise } catch(e) { error = e } expect(() => { throw error }).to.throw('Channel closed') }) }) describe('generate', () => { beforeEach(() => c.start()) it('defaults to empty args', () => { c.generate(['net', 'kb1rd', 'hello']) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [], return_addr: sent_msgs[0][0].return_addr, return_type: 'generator' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('passes args to `send`', async () => { c.generate( ['net', 'kb1rd', 'hello'], [123, 'abc', { [toRpcSerialized]: () => 'hi' }] ) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [123, 'abc', 'hi'], return_addr: sent_msgs[0][0].return_addr, return_type: 'generator' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('generates a unique `return_addr`', () => { c.generate( ['net', 'kb1rd', 'hello'], [123, 'abc', { [toRpcSerialized]: () => 'hi' }] ) c.generate( ['net', 'kb1rd', 'hello'], [123, 'abc', { [toRpcSerialized]: () => 'hi' }] ) expect(sent_msgs.length).to.be.equal(2) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [123, 'abc', 'hi'], return_addr: ['_', 'ret', 'id0'], return_type: 'generator' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(sent_msgs[1][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [123, 'abc', 'hi'], return_addr: ['_', 'ret', 'id1'], return_type: 'generator' }) expect(sent_msgs[1][1].length).to.be.equal(0) }) it('registers handler and returns async', async () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [], return_addr: sent_msgs[0][0].return_addr, return_type: 'generator' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(typeof c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress )).to.be.equal('function') let val = gen.next() ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, []) await val val = gen.next() ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, []) await val }) it('resolves promise with return values', async () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], 'hello', undefined) const data = await gen.next() expect(data.value).to.be.equal('hello') expect(data.done).to.be.equal(false) }) it('finishes if done signal sent', async () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], undefined, undefined, true) const data = await gen.next() expect(data.done).to.be.equal(true) }) it('stops generator if `return` called', () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) gen.return() expect(sent_msgs.length).to.be.equal(2) const raddr = sent_msgs[0][0].return_addr as MultistringAddress expect(sent_msgs[1][0].to).to.be.deep.equal(['_', 'stopgen', ...raddr]) expect(c._i_reg.map.get(raddr)).to.be.undefined }) it('stops generator if `throw` called', () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) gen.throw('hi') expect(sent_msgs.length).to.be.equal(2) const raddr = sent_msgs[0][0].return_addr as MultistringAddress expect(sent_msgs[1][0].to).to.be.deep.equal(['_', 'stopgen', ...raddr]) expect(c._i_reg.map.get(raddr)).to.be.undefined }) it('unregisters on completion', async () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], undefined, undefined, true) const data = await gen.next() expect(data.done).to.be.equal(true) expect( c._i_reg.map.get(sent_msgs[0][0].return_addr as MultistringAddress) ).to.be.undefined }) it('rejects promise and finishes with second error argument', async () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], undefined, 'ERROR!') let error: any try { await gen.next() } catch(e) { error = e } expect(error).to.be.equal('ERROR!') expect((await gen.next()).done).to.be.true }) it('rejects promise if callback called with other channel', async () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(new RpcChannel(() => undefined), []) let error: any try { await gen.next() } catch(e) { error = e } expect(error).to.be.an.instanceOf(InvalidChannelError) }) it('rejects promise with ForwardedError if error passed', async () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], undefined, { name: 'ERROR!' }) let error: any try { await gen.next() } catch(e) { error = e } expect(error).to.be.an.instanceOf(ForwardedError) expect(error.name).to.be.equal('ERROR!') }) it('finishes if channel closed', async () => { const gen = c.generate(['net', 'kb1rd', 'hello'], []) c.close() const data = await gen.next() expect(data.done).to.be.equal(true) }) }) describe('call_obj', () => { beforeEach(() => c.start()) it('passes args to `send`', () => { c.call_obj.net.kb1rd .hello(123, 'abc', { [toRpcSerialized]: () => 'hi' }) .catch(() => undefined) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['net', 'kb1rd', 'hello'], args: [123, 'abc', 'hi'], return_addr: sent_msgs[0][0].return_addr, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('resolves promise with return values', async () => { const promise = c.call_obj.net.kb1rd.hello() ;(c._i_reg.map.get( sent_msgs[0][0].return_addr as MultistringAddress ) as RpcFunction)(c, [], 'hello', undefined) const data = await promise expect(data).to.be.equal('hello') }) it('returns undefined if accessor not string', () => { expect(c.call_obj[(Symbol() as unknown) as string]).to.be.undefined }) }) /* describe('clearPolicy', () => { it('removes already set security policy', () => { c.setPolicy(['yeet'], AccessPolicy.DENY) c.clearPolicy(['yeet']) // Clear default c.clearPolicy([]) expect(c.access.get(['yeet'])).to.be.undefined }) }) */ describe('receive', () => { beforeEach(() => c.start()) it('calls function with arguments', () => { let called = false let error: Error | undefined = undefined c.register(['net', 'kb1rd', 'test'], (chan, wc, a, b) => { called = true // Errors are caught in handlers. Pass them up try { expect(chan).to.be.equal(c) expect(wc.length).to.be.equal(0) expect(a).to.be.equal('hello') expect(b).to.be.equal(123) } catch(e) { e = error } }) c.receive({ to: ['net', 'kb1rd', 'test'], args: ['hello', 123] }) expect(called).to.be.true if (error) { throw error } }) it('does nothing if channel stopped', () => { c.stop() let called = false c.on('rawmessage', () => (called = true)) c.receive({ to: ['net', 'kb1rd', 'test'], args: ['hello', 123] }) expect(called).to.be.false }) describe('promise return', () => { it('`send`s return value', () => { c.register(['net', 'kb1rd', 'add'], (chan, wc, a, b) => { // In reality, schemas would provide runtime type checking return (a as string) + (b as number) }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'] }) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: ['hello123', undefined], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s error', () => { c.register(['net', 'kb1rd', 'add'], (chan, wc, a, b) => { throw new TypeError('yeet') }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'] }) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, { name: 'TypeError', columnNumber: undefined, fileName: undefined, lineNumber: undefined, message: 'yeet', stack: 'Stack trace redacted for security reasons' } ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s async return value', async () => { let promise: Promise<string> | undefined = undefined c.register(['net', 'kb1rd', 'add'], (chan, wc, a, b) => { // In reality, schemas would provide runtime type checking promise = Promise.resolve((a as string) + (b as number)) return promise }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'] }) await promise expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: ['hello123', undefined], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s async first generator value', async () => { c.register(['net', 'kb1rd', 'add'], async function*(chan, wc, a, b) { // In reality, schemas would provide runtime type checking yield (a as string) + (b as number) yield 3 }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'] }) // This code will finish executing before the generator promise is // resolved, so this is a dirty hack to get around that await new Promise((res) => setTimeout(res, static_await_delay)) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: ['hello123', undefined], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s async first generator error', async () => { c.register(['net', 'kb1rd', 'add'], async function*(chan, wc, a, b) { throw 'error' }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'] }) // This code will finish executing before the generator promise is // resolved, so this is a dirty hack to get around that await new Promise((res) => setTimeout(res, static_await_delay)) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [undefined, 'error'], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s async error', async () => { let promise: Promise<string> | undefined = undefined c.register(['net', 'kb1rd', 'add'], (chan, wc, a, b) => { promise = Promise.reject(new TypeError('yeet')) return promise }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'] }) try { await promise } catch(e) {} expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, { name: 'TypeError', columnNumber: undefined, fileName: undefined, lineNumber: undefined, message: 'yeet', stack: 'Stack trace redacted for security reasons' } ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s error if function undefined', () => { c.receive({ to: ['net', 'kb1rd', 'test'], args: [], return_addr: ['return'] }) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, { name: 'TypeError', columnNumber: undefined, fileName: undefined, lineNumber: undefined, message: 'Function at address is undefined', stack: 'Stack trace redacted for security reasons' } ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s error if access denied BEFORE getting function', () => { c.access_controller = new FunctionAccessController((addr) => { expect(addr).to.be.deep.equal(['net', 'kb1rd', 'test']) return false }) c.receive({ to: ['net', 'kb1rd', 'test'], args: [], return_addr: ['return'] }) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, { name: 'AccessDeniedError', columnNumber: undefined, fileName: undefined, lineNumber: undefined, message: 'Access denied', stack: 'Stack trace redacted for security reasons' } ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s error if registry denies access', () => { c._i_reg.access_chain.push(new FunctionAccessController((addr) => { expect(addr).to.be.deep.equal(['net', 'kb1rd', 'test']) return false })) c.receive({ to: ['net', 'kb1rd', 'test'], args: [], return_addr: ['return'] }) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, { name: 'AccessDeniedError', columnNumber: undefined, fileName: undefined, lineNumber: undefined, message: 'Access denied', stack: 'Stack trace redacted for security reasons' } ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) }) describe('generator return', () => { it('`send`s error', () => { c.register(['net', 'kb1rd', 'add'], (chan, wc, a, b) => { throw new TypeError('yeet') }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'], return_type: 'generator' }) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, { name: 'TypeError', columnNumber: undefined, fileName: undefined, lineNumber: undefined, message: 'yeet', stack: 'Stack trace redacted for security reasons' }, true ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s return value, then finishes for basic', () => { c.register(['net', 'kb1rd', 'add'], (chan, wc, a, b) => { // In reality, schemas would provide runtime type checking return (a as string) + (b as number) }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'], return_type: 'generator' }) expect(sent_msgs.length).to.be.equal(2) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: ['hello123', undefined, false], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(sent_msgs[1][0]).to.be.deep.equal({ to: ['return'], args: [undefined, undefined, true], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[1][1].length).to.be.equal(0) }) it('`send`s return value, then finishes for promise', async () => { let promise: Promise<string> | undefined = undefined c.register(['net', 'kb1rd', 'add'], (chan, wc, a, b) => { // In reality, schemas would provide runtime type checking promise = Promise.resolve((a as string) + (b as number)) return promise }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'], return_type: 'generator' }) await promise expect(sent_msgs.length).to.be.equal(2) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: ['hello123', undefined, false], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(sent_msgs[1][0]).to.be.deep.equal({ to: ['return'], args: [undefined, undefined, true], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[1][1].length).to.be.equal(0) }) it('`send`s error for promise', async () => { let promise: Promise<string> | undefined = undefined c.register(['net', 'kb1rd', 'add'], (chan, wc, a, b) => { // In reality, schemas would provide runtime type checking promise = Promise.reject(new TypeError('yeet')) return promise }) c.receive({ to: ['net', 'kb1rd', 'add'], args: ['hello', 123], return_addr: ['return'], return_type: 'generator' }) try { await promise } catch(e) {} expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, { name: 'TypeError', columnNumber: undefined, fileName: undefined, lineNumber: undefined, message: 'yeet', stack: 'Stack trace redacted for security reasons' }, true ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('`send`s multiple data values, then finishes', async () => { c.register(['net', 'kb1rd', 'test'], async function*(chan, wc, a, b) { yield 'a' yield 'b' yield 'c' }) c.receive({ to: ['net', 'kb1rd', 'test'], args: [], return_addr: ['return'], return_type: 'generator' }) await new Promise((res) => setTimeout(res, static_await_delay)) expect(sent_msgs.length).to.be.equal(4) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ 'a', undefined, false ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(sent_msgs[1][0]).to.be.deep.equal({ to: ['return'], args: [ 'b', undefined, false ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[1][1].length).to.be.equal(0) expect(sent_msgs[2][0]).to.be.deep.equal({ to: ['return'], args: [ 'c', undefined, false ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[2][1].length).to.be.equal(0) expect(sent_msgs[3][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, undefined, true ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[3][1].length).to.be.equal(0) }) it('responds to stop handler', async () => { let on_stop: () => void let stop_promise = new Promise((r) => (on_stop = r)) c.register(['net', 'kb1rd', 'test'], async function*(chan, wc, a, b) { yield 'a' on_stop() yield 'b' yield 'c' }) c.receive({ to: ['net', 'kb1rd', 'test'], args: [], return_addr: ['return'], return_type: 'generator' }) await stop_promise c.receive({ to: ['_', 'stopgen', 'return'], args: [] }) await new Promise((res) => setTimeout(res, static_await_delay)) expect(sent_msgs.length).to.be.equal(1) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ 'a', undefined, false ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) }) it('ends on error, then stops', async () => { c.register(['net', 'kb1rd', 'test'], async function*(chan, wc, a, b) { yield 'a' throw 'b' yield 'c' }) c.receive({ to: ['net', 'kb1rd', 'test'], args: [], return_addr: ['return'], return_type: 'generator' }) await new Promise((res) => setTimeout(res, static_await_delay)) expect(sent_msgs.length).to.be.equal(2) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ 'a', undefined, false ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(sent_msgs[1][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, 'b', true ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[1][1].length).to.be.equal(0) }) it('unregisters `stopgen` channel when stopped', async () => { c.register(['net', 'kb1rd', 'test'], async function*(chan, wc, a, b) { yield 'a' }) c.receive({ to: ['net', 'kb1rd', 'test'], args: [], return_addr: ['return'], return_type: 'generator' }) await new Promise((res) => setTimeout(res, static_await_delay)) expect(sent_msgs.length).to.be.equal(2) expect(sent_msgs[0][0]).to.be.deep.equal({ to: ['return'], args: [ 'a', undefined, false ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[0][1].length).to.be.equal(0) expect(sent_msgs[1][0]).to.be.deep.equal({ to: ['return'], args: [ undefined, undefined, true ], return_addr: undefined, return_type: 'promise' }) expect(sent_msgs[1][1].length).to.be.equal(0) expect(c.reg.map.get(['_', 'stopgen', 'return'])).to.be.undefined }) }) }) })