From 46ef24100ae80f5495b5ba47a967592c99727fea Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:05:41 -0500 Subject: [PATCH 01/22] feat(extension): Add stream envelope handler. --- packages/extension/src/envelope.test.ts | 110 ++++++++++++++++ packages/extension/src/envelope.ts | 166 ++++++++++++++++++++++-- packages/extension/src/message.ts | 3 + 3 files changed, 267 insertions(+), 12 deletions(-) create mode 100644 packages/extension/src/envelope.test.ts diff --git a/packages/extension/src/envelope.test.ts b/packages/extension/src/envelope.test.ts new file mode 100644 index 000000000..1422370e9 --- /dev/null +++ b/packages/extension/src/envelope.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect } from 'vitest'; + +import { + capTpEnveloper, + commandEnveloper, + EnvelopeLabel, + isStreamEnvelope, + makeStreamEnvelopeHandler, +} from './envelope.js'; +import { Command } from './message.js'; + +const contentFixtures = { + command: { id: '1', message: { type: Command.Evaluate, data: '1 + 1' } }, + capTp: { id: '4', message: { type: 'CTP_CALL', epoch: 0 } }, +}; + +describe('envelope', () => { + describe('isStreamEnvelope', () => { + it('returns true for valid messages', () => { + expect( + isStreamEnvelope({ + label: EnvelopeLabel.Command, + content: { + id: '1', + message: { type: 'evaluate', data: '1 + 1' }, + }, + }), + ).toBe(true); + }); + + it.each` + exportName | enveloper | content + ${'commandEnveloper'} | ${commandEnveloper} | ${contentFixtures.command} + ${'capTpEnveloper'} | ${capTpEnveloper} | ${contentFixtures.capTp} + `( + 'returns true for valid contents wrapped by $enveloper.label', + ({ enveloper, content }) => { + expect(isStreamEnvelope(enveloper.wrap(content))).toBe(true); + }, + ); + + it.each` + envelope + ${{}} + ${{ label: EnvelopeLabel.Command }} + ${{ label: EnvelopeLabel.CapTp }} + ${{ label: EnvelopeLabel.Command, content: contentFixtures.capTp }} + ${{ label: 'unknown', content: [] }} + `('returns false for invalid envelopes: $envelope', ({ envelope }) => { + expect(isStreamEnvelope(envelope)).toBe(false); + }); + }); + + describe('StreamEnvelopeHandler', () => { + const testEnvelopeHandlers = { + command: async () => EnvelopeLabel.Command, + capTp: async () => EnvelopeLabel.CapTp, + }; + const testErrorHandler = (problem: unknown): never => { + throw new Error(`TEST ${String(problem)}`); + }; + const handler = makeStreamEnvelopeHandler( + testEnvelopeHandlers, + testErrorHandler, + ); + + it.each` + enveloper | message + ${commandEnveloper} | ${contentFixtures.command} + ${capTpEnveloper} | ${contentFixtures.capTp} + `('handles valid StreamEnvelopes', async ({ enveloper, message }) => { + expect(await handler(enveloper.wrap(message))).toStrictEqual( + enveloper.label, + ); + }); + + it('routes invalid envelopes to supplied error handler', async () => { + await expect(handler({ label: 'unknown', content: [] })).rejects.toThrow( + /TEST Stream envelope handler received unexpected value/u, + ); + }); + + it('routes valid stream envelopes with an unhandled label to the error handler', async () => { + const commandHandler = makeStreamEnvelopeHandler( + { command: testEnvelopeHandlers.command }, + testErrorHandler, + ); + await expect( + commandHandler(capTpEnveloper.wrap(contentFixtures.capTp)), + ).rejects.toThrow( + /TEST Stream envelope handler received an envelope with known but unexpected label/u, + ); + }); + }); + + describe('envelopeKit', () => { + it.each` + enveloper | envelope + ${commandEnveloper} | ${contentFixtures.capTp} + ${capTpEnveloper} | ${contentFixtures.command} + `( + 'throws when unwrapping a malformed envelope', + ({ enveloper, envelope }) => { + expect(() => enveloper.unwrap(envelope)).toThrow( + /Expected envelope labelled/u, + ); + }, + ); + }); +}); diff --git a/packages/extension/src/envelope.ts b/packages/extension/src/envelope.ts index cc5e72e37..9951f2d27 100644 --- a/packages/extension/src/envelope.ts +++ b/packages/extension/src/envelope.ts @@ -1,22 +1,164 @@ import { isObject } from '@metamask/utils'; -import type { WrappedIframeMessage } from './message.js'; -import { isWrappedIframeMessage } from './message.js'; +import type { CapTpMessage, WrappedIframeMessage } from './message.js'; +import { isCapTpMessage, isWrappedIframeMessage } from './message.js'; export enum EnvelopeLabel { Command = 'command', CapTp = 'capTp', } -export type StreamEnvelope = - | { - label: EnvelopeLabel.Command; - content: WrappedIframeMessage; - } - | { label: EnvelopeLabel.CapTp; content: unknown }; +type EnvelopeForm