import { createHostBridge, HostBridgeMessage, HostBridgeMessaging } from '../../utils/host_bridge' const createFakeMessaging = () => { const listeners: ((event: HostBridgeMessage) => void)[] = [] const sent: HostBridgeMessage[] = [] const remove = jest.fn() const messaging: HostBridgeMessaging = { addMessageListener: handler => { listeners.push(handler) return { remove } }, sendMessage: message => { sent.push(message) }, } return { messaging, sent, remove, emit: (event: HostBridgeMessage) => { listeners.forEach(l => l(event)) }, } } describe('createHostBridge', () => { describe('request', () => { it('resolves with the host reply when requestId matches', async () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) const promise = bridge.request<{ accessToken: string }>('refreshAccessToken') const sent = fake.sent[0] fake.emit({ requestId: sent.requestId as string, accessToken: 'abc' }) await expect(promise).resolves.toEqual({ requestId: sent.requestId, accessToken: 'abc' }) }) it('rejects with an Error when the host reply carries an error', async () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) const promise = bridge.request('refreshAccessToken') const sent = fake.sent[0] fake.emit({ requestId: sent.requestId as string, error: 'boom' }) await expect(promise).rejects.toThrow('boom') }) it('ignores replies with non-matching requestIds', async () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) const inFlight = bridge.request('refreshAccessToken') fake.emit({ requestId: 'unrelated', accessToken: 'wrong' }) const sent = fake.sent[0] fake.emit({ requestId: sent.requestId as string, accessToken: 'right' }) await expect(inFlight).resolves.toMatchObject({ accessToken: 'right' }) }) it('produces unique requestIds across calls', () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) bridge.request('refreshAccessToken') bridge.request('refreshAccessToken') expect(fake.sent[0].requestId).not.toEqual(fake.sent[1].requestId) }) it('forwards the type and payload to sendMessage with a requestId', () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) bridge.request('shouldRefreshAccessToken', { responseCode: 401 }) expect(fake.sent[0]).toMatchObject({ type: 'shouldRefreshAccessToken', responseCode: 401, }) expect(typeof fake.sent[0].requestId).toBe('string') }) it('payload cannot override the type or requestId routing keys', () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) bridge.request('refreshAccessToken', { type: 'somethingElse', requestId: 'attacker-controlled', }) const sent = fake.sent[0] expect(sent.type).toBe('refreshAccessToken') expect(sent.requestId).not.toBe('attacker-controlled') expect(typeof sent.requestId).toBe('string') }) }) describe('send', () => { it('sends type + payload without a requestId', () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) bridge.send('setChannelId', { channelId: 'c-123' }) expect(fake.sent).toHaveLength(1) expect(fake.sent[0]).toEqual({ type: 'setChannelId', channelId: 'c-123' }) expect(fake.sent[0].requestId).toBeUndefined() }) it('payload cannot override the type', () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) bridge.send('setChannelId', { type: 'somethingElse', channelId: 'c-123' }) expect(fake.sent[0].type).toBe('setChannelId') }) }) describe('subscribe', () => { type ConfigEvent = { type: 'chatChangeConfig'; config: { foo: string } } type ClearCacheEvent = { type: 'chatClearCache' } type AnyEvent = ConfigEvent | ClearCacheEvent it('invokes the handler when the event type matches', () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) const handler = jest.fn() bridge.subscribe('chatChangeConfig', handler) fake.emit({ type: 'chatChangeConfig', config: { foo: 'bar' } }) expect(handler).toHaveBeenCalledWith({ type: 'chatChangeConfig', config: { foo: 'bar' } }) }) it('does not invoke the handler when the event type differs', () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) const handler = jest.fn() bridge.subscribe('chatChangeConfig', handler) fake.emit({ type: 'chatClearCache' }) expect(handler).not.toHaveBeenCalled() }) it('returns an unsubscribe function that removes the listener', () => { const fake = createFakeMessaging() const bridge = createHostBridge(fake.messaging) const handler = jest.fn() const unsubscribe = bridge.subscribe('chatChangeConfig', handler) unsubscribe() expect(fake.remove).toHaveBeenCalledTimes(1) }) }) })