@bsv/p2p
Version:
A client for P2P messaging and payments
525 lines (411 loc) • 17.9 kB
text/typescript
/* 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')
})
})