rpc.client.spec.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243
  1. /** The Connection carrier face: payload forms, streams, unmatched rejections, and abort. */
  2. import { describe, expect, it } from 'vitest'
  3. import { RemoteMock, ok, openStream } from '../src/index.ts'
  4. const idle = (): AbortSignal => new AbortController().signal
  5. describe('RemoteMock.rpc', () => {
  6. it('calls and opens by endpoint, taking the proxies\' positional args or the Gateway\'s single object', async () => {
  7. const mock = RemoteMock.create()
  8. .unary('session/list', ok({ items: [] }))
  9. .unary('$events/result', (...args) => ok(args))
  10. .stream('session/control', openStream([{ type: 'baseline' }]))
  11. await expect(mock.rpc.call('/api', 'session/list', { args: [{}] })).resolves.toEqual({ ok: true, value: { items: [] } })
  12. await expect(mock.rpc.call('/api', '$events/result', { args: { clientId: 'c' } }, idle())).resolves.toEqual({ ok: true, value: [{ clientId: 'c' }] })
  13. const stream = mock.rpc.open!('/api', 'session/control', { args: [{ since: 1 }] }, idle())[Symbol.asyncIterator]()
  14. await expect(stream.next()).resolves.toEqual({ value: { type: 'baseline' }, done: false })
  15. expect(mock.log.calls().map(call => [call.endpoint, call.args])).toEqual([['session/list', [{}]], ['$events/result', [{ clientId: 'c' }]]])
  16. expect(mock.log.streams('session/control').map(open => open.args)).toEqual([[{ since: 1 }]])
  17. })
  18. it('rejects malformed payloads and unmatched endpoints, logging the miss', async () => {
  19. const mock = RemoteMock.create()
  20. await expect(mock.rpc.call('/api', 'a/b', 'bare')).rejects.toThrow('remote-mock: payload of a/b must be { args: unknown[] | object }')
  21. await expect(mock.rpc.call('/api', 'a/b', { args: 'x' })).rejects.toThrow('must be { args: unknown[] | object }')
  22. expect(() => mock.rpc.open!('/api', 'a/b', null, idle())).toThrow('payload of a/b must be { args: unknown[] | object }')
  23. await expect(mock.rpc.call('/api', 'a/b', { args: [] })).rejects.toThrow('remote-mock: no rule for a/b')
  24. expect(mock.log.unmatched()).toEqual([{ endpoint: 'a/b', mode: 'unary' }])
  25. })
  26. it('rejects an aborted call: immediately when already aborted, with the reason while the rule is pending', async () => {
  27. const mock = RemoteMock.create().unary('slow/call', () => new Promise(() => {}))
  28. const pending = new AbortController()
  29. const request = mock.rpc.call('/api', 'slow/call', { args: [] }, pending.signal)
  30. pending.abort(new Error('caller left'))
  31. await expect(request).rejects.toThrow('caller left')
  32. const already = new AbortController()
  33. already.abort('not an error')
  34. await expect(mock.rpc.call('/api', 'slow/call', { args: [] }, already.signal)).rejects.toThrow('remote-mock: call aborted')
  35. await expect(mock.rpc.call('/api', 'session/list', { args: [] }, idle())).rejects.toThrow('no rule for session/list')
  36. // The abort rejects the caller; the never-settling rules stay pending in the log, and the miss is not a call.
  37. expect(mock.log.calls().map(call => [call.endpoint, call.state])).toEqual([['slow/call', 'pending'], ['slow/call', 'pending']])
  38. })
  39. })