UNPKG

@bsv/p2p

Version:

A client for P2P messaging and payments

525 lines (411 loc) 17.9 kB
/* eslint-env jest */ import { MessageBoxClient } from '../MessageBoxClient.js' import { WalletClient, AuthFetch } from '@bsv/sdk' // --- MOCK: WalletClient methods --- jest.spyOn(WalletClient.prototype, 'createHmac').mockResolvedValue({ hmac: Array.from(new Uint8Array([1, 2, 3])) }) jest.spyOn(WalletClient.prototype, 'getPublicKey').mockResolvedValue({ publicKey: 'mockIdentityKey' }) // --- MOCK: AuthFetch responses --- const defaultMockResponse: Partial<Response> = { json: async () => ({ status: 'success', message: 'Mocked response' }), headers: new Headers(), ok: true, status: 200 } jest.spyOn(AuthFetch.prototype, 'fetch') .mockResolvedValue(defaultMockResponse as Response) const socketOnMap: Record<string, (...args: any[]) => void> = {} const mockSocket = { on: jest.fn((event, callback) => { socketOnMap[event] = callback }), emit: jest.fn(), disconnect: jest.fn(), connected: true, off: jest.fn() } jest.mock('@bsv/authsocket-client', () => ({ AuthSocketClient: jest.fn(() => mockSocket) })) // Optional: Global WebSocket override (not strictly needed with AuthSocketClient) class MockWebSocket { static CONNECTING = 0 static OPEN = 1 static CLOSING = 2 static CLOSED = 3 readyState = MockWebSocket.OPEN on = jest.fn() send = jest.fn() close = jest.fn() } global.WebSocket = MockWebSocket as unknown as typeof WebSocket describe('MessageBoxClient', () => { let mockWalletClient: WalletClient beforeEach(() => { mockWalletClient = new WalletClient() jest.clearAllMocks() }) const VALID_SEND_RESULT = { body: JSON.stringify({ status: 200, message: 'Your message has been sent!' }) } const VALID_LIST_AND_READ_RESULT = { body: JSON.stringify({ status: 200, messages: [ { sender: 'mockSender', messageBoxId: 42, body: '{}' }, { sender: 'mockSender', messageBoxId: 43, body: '{}' } ] }) } const VALID_ACK_RESULT = { body: JSON.stringify({ status: 200, message: 'Messages marked as acknowledged!' }) } it('Creates an instance of the MessageBoxClient class', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient, enableLogging: true }) expect(messageBoxClient).toHaveProperty('host', 'https://messagebox.babbage.systems') // Ensure the socket is initialized as undefined before connecting expect(messageBoxClient.testSocket).toBeUndefined() }) it('Initializes WebSocket connection', async () => { await new Promise(resolve => setTimeout(resolve, 100)) const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient, enableLogging: true }) const connection = messageBoxClient.initializeConnection() // Simulate server response setTimeout(() => { socketOnMap['authenticationSuccess']?.({ status: 'ok' }) }, 100) await expect(connection).resolves.toBeUndefined() }, 10000) it('Falls back to HTTP when WebSocket is not initialized', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) // Bypass the real connection logic jest.spyOn(messageBoxClient, 'initializeConnection').mockImplementation(async () => { }) // Manually set identity key ; (messageBoxClient as any).myIdentityKey = 'mockIdentityKey' // Simulate WebSocket not initialized ; (messageBoxClient as any).socket = null // Expect it to fall back to HTTP and succeed const result = await messageBoxClient.sendLiveMessage({ recipient: 'mockIdentityKey', messageBox: 'test_inbox', body: 'Test message' }) expect(result).toEqual({ status: 'success', message: 'Mocked response', messageId: '010203' }) }) it('Listens for live messages', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) const connection = messageBoxClient.initializeConnection() setTimeout(() => { socketOnMap['authenticationSuccess']?.({ status: 'ok' }) }, 100) await connection const mockOnMessage = jest.fn() await messageBoxClient.listenForLiveMessages({ messageBox: 'test_inbox', onMessage: mockOnMessage }) expect(messageBoxClient.testSocket?.emit).toHaveBeenCalledWith( 'joinRoom', 'mockIdentityKey-test_inbox' ) }, 10000) it('Sends a live message', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) const connection = messageBoxClient.initializeConnection() // Simulate WebSocket auth success setTimeout(() => { socketOnMap['authenticationSuccess']?.({ status: 'ok' }) }, 100) await connection const emitSpy = jest.spyOn(messageBoxClient.testSocket as any, 'emit') // Kick off sending a message (this sets up the ack listener) const sendPromise = messageBoxClient.sendLiveMessage({ recipient: 'mockIdentityKey', messageBox: 'test_inbox', body: 'Test message' }) // Simulate WebSocket acknowledgment setTimeout(() => { socketOnMap['sendMessageAck-mockIdentityKey-test_inbox']?.({ status: 'success', messageId: 'mocked123' }) }, 100) const result = await sendPromise // Check that WebSocket emit happened correctly expect(emitSpy).toHaveBeenCalledWith( 'sendMessage', expect.objectContaining({ roomId: 'mockIdentityKey-test_inbox', message: expect.objectContaining({ body: 'Test message' }) }) ) // Check the resolved result expect(result).toEqual({ status: 'success', messageId: 'mocked123' }) }, 15000) it('Sends a message', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({ json: async () => ({ status: 'success', message: 'Your message has been sent!' }), headers: new Headers(), ok: true, status: 200 } as unknown as Response) const result = await messageBoxClient.sendMessage({ recipient: 'mockIdentityKey', messageBox: 'test_inbox', body: { data: 'test' } }) expect(result).toHaveProperty('message', 'Your message has been sent!') }) it('Lists available messages', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({ json: async () => JSON.parse(VALID_LIST_AND_READ_RESULT.body), headers: new Headers(), ok: true, status: 200 } as unknown as Response) const result = await messageBoxClient.listMessages({ messageBox: 'test_inbox' }) expect(result).toEqual(JSON.parse(VALID_LIST_AND_READ_RESULT.body).messages) }) it('Acknowledges a message', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({ json: async () => JSON.parse(VALID_ACK_RESULT.body), headers: new Headers(), ok: true, status: 200 } as unknown as Response) const result = await messageBoxClient.acknowledgeMessage({ messageIds: ['42'] }) expect(result).toEqual(200) }) it('Throws an error when sendMessage() API fails', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) jest.spyOn(messageBoxClient.authFetch, 'fetch') .mockResolvedValue({ status: 500, statusText: 'Internal Server Error', ok: false, json: async () => ({ status: 'error', description: 'Internal Server Error' }), headers: new Headers() } as unknown as Response) await expect(messageBoxClient.sendMessage({ recipient: 'mockIdentityKey', messageBox: 'test_inbox', body: 'Test Message' })).rejects.toThrow('Message sending failed: HTTP 500 - Internal Server Error') }) it('Throws an error when listMessages() API fails', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) jest.spyOn(messageBoxClient.authFetch, 'fetch') .mockResolvedValue({ status: 500, json: async () => ({ status: 'error', description: 'Failed to fetch messages' }) } as unknown as Response) await expect(messageBoxClient.listMessages({ messageBox: 'test_inbox' })) .rejects.toThrow('Failed to fetch messages') }) it('Throws an error when acknowledgeMessage() API fails', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) jest.spyOn(messageBoxClient.authFetch, 'fetch') .mockResolvedValue({ status: 500, json: async () => ({ status: 'error', description: 'Failed to acknowledge messages' }) } as unknown as Response) await expect(messageBoxClient.acknowledgeMessage({ messageIds: ['42'] })) .rejects.toThrow('Failed to acknowledge messages') }) it('Throws an error when identity key is missing', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) // Mock `getPublicKey` to return an empty key jest.spyOn(mockWalletClient, 'getPublicKey').mockResolvedValue({ publicKey: '' }) await expect(messageBoxClient.initializeConnection()).rejects.toThrow('Identity key is missing') }) it('Throws an error when WebSocket is not initialized before listening for messages', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) // Stub out the identity key to pass that check ; (messageBoxClient as any).myIdentityKey = 'mockIdentityKey' // Stub out joinRoom to throw like the real one might jest.spyOn(messageBoxClient, 'joinRoom').mockRejectedValue(new Error('WebSocket connection not initialized')) await expect( messageBoxClient.listenForLiveMessages({ onMessage: jest.fn(), messageBox: 'test_inbox' }) ).rejects.toThrow('WebSocket connection not initialized') }) it('Emits joinRoom event and listens for incoming messages', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) // Mock identity key properly jest.spyOn(mockWalletClient, 'getPublicKey').mockResolvedValue({ publicKey: 'mockIdentityKey' }) // Mock socket with `on` method capturing event handlers const mockSocket = { emit: jest.fn(), on: jest.fn() } as any // Mock `initializeConnection` so it assigns `socket` & identity key jest.spyOn(messageBoxClient, 'initializeConnection').mockImplementation(async () => { Object.defineProperty(messageBoxClient, 'testIdentityKey', { get: () => 'mockIdentityKey' }) Object.defineProperty(messageBoxClient, 'testSocket', { get: () => mockSocket }); (messageBoxClient as any).socket = mockSocket; // Ensures internal socket is set (messageBoxClient as any).myIdentityKey = 'mockIdentityKey' // Ensures identity key is set }) const onMessageMock = jest.fn() await messageBoxClient.listenForLiveMessages({ onMessage: onMessageMock, messageBox: 'test_inbox' }) // Ensure `joinRoom` event was emitted with the correct identity key expect(mockSocket.emit).toHaveBeenCalledWith('joinRoom', 'mockIdentityKey-test_inbox') // Simulate receiving a message const receivedMessage = { text: 'Hello, world!' } // Extract & invoke the callback function stored in `on` const sendMessageCallback = mockSocket.on.mock.calls.find( ([eventName]) => eventName === 'sendMessage-mockIdentityKey-test_inbox' )?.[1] // Extract the callback function if (typeof sendMessageCallback === 'function') { sendMessageCallback(receivedMessage) } // Ensure `onMessage` was called with the received message expect(onMessageMock).toHaveBeenCalledWith(receivedMessage) }) it('Handles WebSocket connection and disconnection events', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) // Simulate identity key jest.spyOn(mockWalletClient, 'getPublicKey').mockResolvedValue({ publicKey: 'mockIdentityKey' }) // Simulate connection + disconnection + auth success setTimeout(() => { socketOnMap['connect']?.() socketOnMap['disconnect']?.() socketOnMap['authenticationSuccess']?.({ status: 'ok' }) }, 100) await messageBoxClient.initializeConnection() // Verify event listeners were registered expect(mockSocket.on).toHaveBeenCalledWith('connect', expect.any(Function)) expect(mockSocket.on).toHaveBeenCalledWith('disconnect', expect.any(Function)) }, 10000) it('throws an error when recipient is empty in sendLiveMessage', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) // Mock `initializeConnection` so it assigns `socket` & identity key jest.spyOn(messageBoxClient, 'initializeConnection').mockImplementation(async () => { Object.defineProperty(messageBoxClient, 'testIdentityKey', { get: () => 'mockIdentityKey' }) Object.defineProperty(messageBoxClient, 'testSocket', { get: () => mockSocket }); (messageBoxClient as any).socket = mockSocket; // Ensures internal socket is set (messageBoxClient as any).myIdentityKey = 'mockIdentityKey' // Ensures identity key is set }) // Mock socket to ensure WebSocket validation does not fail const mockSocket = { emit: jest.fn() } as any jest.spyOn(messageBoxClient, 'testSocket', 'get').mockReturnValue(mockSocket) await expect(messageBoxClient.sendLiveMessage({ recipient: ' ', messageBox: 'test_inbox', body: 'Test message' })).rejects.toThrow('[MB CLIENT ERROR] Recipient identity key is required') }) it('throws an error when recipient is missing in sendMessage', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) await expect(messageBoxClient.sendMessage({ recipient: '', // Empty recipient messageBox: 'test_inbox', body: 'Test message' })).rejects.toThrow('You must provide a message recipient!') await expect(messageBoxClient.sendMessage({ recipient: ' ', // Whitespace recipient messageBox: 'test_inbox', body: 'Test message' })).rejects.toThrow('You must provide a message recipient!') await expect(messageBoxClient.sendMessage({ recipient: null as any, // Null recipient messageBox: 'test_inbox', body: 'Test message' })).rejects.toThrow('You must provide a message recipient!') }) it('throws an error when messageBox is missing in sendMessage', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) await expect(messageBoxClient.sendMessage({ recipient: 'mockIdentityKey', messageBox: '', // Empty messageBox body: 'Test message' })).rejects.toThrow('You must provide a messageBox to send this message into!') await expect(messageBoxClient.sendMessage({ recipient: 'mockIdentityKey', messageBox: ' ', // Whitespace messageBox body: 'Test message' })).rejects.toThrow('You must provide a messageBox to send this message into!') await expect(messageBoxClient.sendMessage({ recipient: 'mockIdentityKey', messageBox: null as any, // Null messageBox body: 'Test message' })).rejects.toThrow('You must provide a messageBox to send this message into!') }) it('throws an error when message body is missing in sendMessage', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) await expect(messageBoxClient.sendMessage({ recipient: 'mockIdentityKey', messageBox: 'test_inbox', body: '' // Empty body })).rejects.toThrow('Every message must have a body!') await expect(messageBoxClient.sendMessage({ recipient: 'mockIdentityKey', messageBox: 'test_inbox', body: ' ' // Whitespace body })).rejects.toThrow('Every message must have a body!') await expect(messageBoxClient.sendMessage({ recipient: 'mockIdentityKey', messageBox: 'test_inbox', body: null as any // Null body })).rejects.toThrow('Every message must have a body!') }) it('throws an error when messageBox is empty in listMessages', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) await expect(messageBoxClient.listMessages({ messageBox: '' // Empty messageBox })).rejects.toThrow('MessageBox cannot be empty') await expect(messageBoxClient.listMessages({ messageBox: ' ' // Whitespace messageBox })).rejects.toThrow('MessageBox cannot be empty') }) it('throws an error when messageIds is empty in acknowledgeMessage', async () => { const messageBoxClient = new MessageBoxClient({ walletClient: mockWalletClient }) await expect(messageBoxClient.acknowledgeMessage({ messageIds: [] // Empty array })).rejects.toThrow('Message IDs array cannot be empty') await expect(messageBoxClient.acknowledgeMessage({ messageIds: undefined as any // Undefined value })).rejects.toThrow('Message IDs array cannot be empty') await expect(messageBoxClient.acknowledgeMessage({ messageIds: null as any // Null value })).rejects.toThrow('Message IDs array cannot be empty') await expect(messageBoxClient.acknowledgeMessage({ messageIds: 'invalid' as any // Not an array })).rejects.toThrow('Message IDs array cannot be empty') }) })