@bsv/message-box-client 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,763 @@
1
+ /* eslint-env jest */
2
+ import { MessageBoxClient } from '../MessageBoxClient.js'
3
+ import { WalletClient, AuthFetch, Transaction, LockingScript } from '@bsv/sdk'
4
+
5
+ // MOCK: WalletClient methods globally
6
+ jest.spyOn(WalletClient.prototype, 'createHmac').mockResolvedValue({
7
+ hmac: Array.from(new Uint8Array([1, 2, 3]))
8
+ })
9
+
10
+ jest.spyOn(WalletClient.prototype, 'getPublicKey').mockResolvedValue({
11
+ publicKey: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
12
+ })
13
+
14
+ jest.spyOn(WalletClient.prototype, 'encrypt').mockResolvedValue({
15
+ ciphertext: [9, 9, 9, 9]
16
+ })
17
+
18
+ jest.spyOn(WalletClient.prototype, 'decrypt').mockResolvedValue({
19
+ plaintext: [1, 2, 3, 4, 5]
20
+ })
21
+
22
+ jest.spyOn(WalletClient.prototype, 'createSignature').mockResolvedValue({
23
+ signature: [1, 2, 3, 4, 5] // <-- any dummy byte array
24
+ })
25
+
26
+ jest.spyOn(WalletClient.prototype, 'connectToSubstrate').mockImplementation(async function () {
27
+ this.substrate = {
28
+ createSignature: async (message: Uint8Array) => {
29
+ return Array.from(new Uint8Array([1, 2, 3])) // Return mock signature
30
+ }
31
+ }
32
+ })
33
+
34
+ const minimalTx = new Transaction()
35
+ minimalTx.addOutput({
36
+ satoshis: 1,
37
+ lockingScript: new LockingScript([]) // ✅ must wrap in LockingScript class
38
+ })
39
+
40
+ const realAtomicBEEF = minimalTx.toAtomicBEEF()
41
+
42
+ jest.spyOn(WalletClient.prototype, 'createAction').mockResolvedValue({
43
+ txid: 'mocked-txid',
44
+ tx: realAtomicBEEF
45
+ })
46
+
47
+ // MOCK: AuthFetch responses
48
+ const defaultMockResponse: Partial<Response> = {
49
+ json: async () => ({ status: 'success', message: 'Mocked response' }),
50
+ headers: new Headers(),
51
+ ok: true,
52
+ status: 200
53
+ }
54
+
55
+ jest.spyOn(MessageBoxClient.prototype as any, 'anointHost').mockImplementation(async () => {
56
+ return { txid: 'mocked-anoint-txid' }
57
+ })
58
+
59
+ jest.spyOn(MessageBoxClient.prototype as any, 'queryAdvertisements')
60
+ .mockResolvedValue([] as string[])
61
+
62
+ jest.spyOn(AuthFetch.prototype, 'fetch')
63
+ .mockResolvedValue(defaultMockResponse as Response)
64
+
65
+ // MOCK: WebSocket behavior
66
+ const socketOnMap: Record<string, (...args: any[]) => void> = {}
67
+
68
+ const mockSocket = {
69
+ on: jest.fn((event, callback) => {
70
+ socketOnMap[event] = callback
71
+ }),
72
+ emit: jest.fn(),
73
+ disconnect: jest.fn(),
74
+ connected: true,
75
+ off: jest.fn()
76
+ }
77
+
78
+ jest.mock('@bsv/authsocket-client', () => ({
79
+ AuthSocketClient: jest.fn(() => mockSocket)
80
+ }))
81
+
82
+ // Optional: Global WebSocket override (not strictly needed with AuthSocketClient)
83
+ class MockWebSocket {
84
+ static CONNECTING = 0
85
+ static OPEN = 1
86
+ static CLOSING = 2
87
+ static CLOSED = 3
88
+
89
+ readyState = MockWebSocket.OPEN
90
+ on = jest.fn()
91
+ send = jest.fn()
92
+ close = jest.fn()
93
+ }
94
+ global.WebSocket = MockWebSocket as unknown as typeof WebSocket
95
+
96
+ describe('MessageBoxClient', () => {
97
+ let mockWalletClient: WalletClient
98
+
99
+ beforeEach(() => {
100
+ mockWalletClient = new WalletClient()
101
+
102
+ jest.clearAllMocks()
103
+ // (Optional, but if you want per-test control, you could move mocks here instead of globally.)
104
+ })
105
+
106
+ const VALID_LIST_AND_READ_RESULT = {
107
+ body: JSON.stringify({
108
+ status: 200,
109
+ messages: [
110
+ { sender: 'mockSender', messageId: 42, body: {} },
111
+ { sender: 'mockSender', messageId: 43, body: {} }
112
+ ]
113
+ })
114
+ }
115
+
116
+ const VALID_ACK_RESULT = {
117
+ body: JSON.stringify({
118
+ status: 200,
119
+ message: 'Messages marked as acknowledged!'
120
+ })
121
+ }
122
+
123
+ it('Creates an instance of the MessageBoxClient class', async () => {
124
+ const messageBoxClient = new MessageBoxClient({
125
+ walletClient: mockWalletClient,
126
+ host: 'https://messagebox.babbage.systems',
127
+ enableLogging: true
128
+ })
129
+
130
+ await messageBoxClient.init()
131
+
132
+ expect(messageBoxClient).toHaveProperty('host', 'https://messagebox.babbage.systems')
133
+
134
+ // Ensure the socket is initialized as undefined before connecting
135
+ expect(messageBoxClient.testSocket).toBeUndefined()
136
+ })
137
+
138
+ it('Initializes WebSocket connection', async () => {
139
+ await new Promise(resolve => setTimeout(resolve, 100))
140
+
141
+ const messageBoxClient = new MessageBoxClient({
142
+ walletClient: mockWalletClient,
143
+ host: 'https://messagebox.babbage.systems',
144
+ enableLogging: true
145
+ })
146
+
147
+ await messageBoxClient.init()
148
+
149
+ const connection = messageBoxClient.initializeConnection()
150
+
151
+ // Simulate server response
152
+ setTimeout(() => {
153
+ socketOnMap.authenticationSuccess?.({ status: 'ok' })
154
+ }, 100)
155
+
156
+ await expect(connection).resolves.toBeUndefined()
157
+ }, 10000)
158
+
159
+ it('Falls back to HTTP when WebSocket is not initialized', async () => {
160
+ const messageBoxClient = new MessageBoxClient({
161
+ walletClient: mockWalletClient,
162
+ host: 'https://messagebox.babbage.systems',
163
+ enableLogging: true
164
+ })
165
+
166
+ await messageBoxClient.init()
167
+
168
+ // Bypass the real connection logic
169
+ jest.spyOn(messageBoxClient, 'initializeConnection').mockImplementation(async () => { })
170
+
171
+ // Manually set identity key
172
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
173
+
174
+ // Simulate WebSocket not initialized
175
+ ; (messageBoxClient as any).socket = null
176
+
177
+ // Expect it to fall back to HTTP and succeed
178
+ const result = await messageBoxClient.sendLiveMessage({
179
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
180
+ messageBox: 'test_inbox',
181
+ body: 'Test message'
182
+ })
183
+
184
+ expect(result).toEqual({
185
+ status: 'success',
186
+ message: 'Mocked response',
187
+ messageId: '010203'
188
+ })
189
+ })
190
+
191
+ it('Listens for live messages', async () => {
192
+ const messageBoxClient = new MessageBoxClient({
193
+ walletClient: mockWalletClient,
194
+ host: 'https://messagebox.babbage.systems',
195
+ enableLogging: true
196
+ })
197
+
198
+ await messageBoxClient.init()
199
+
200
+ const connection = messageBoxClient.initializeConnection()
201
+
202
+ setTimeout(() => {
203
+ socketOnMap.authenticationSuccess?.({ status: 'ok' })
204
+ }, 100)
205
+
206
+ await connection
207
+
208
+ const mockOnMessage = jest.fn()
209
+
210
+ await messageBoxClient.listenForLiveMessages({
211
+ messageBox: 'test_inbox',
212
+ onMessage: mockOnMessage
213
+ })
214
+
215
+ expect(messageBoxClient.testSocket?.emit).toHaveBeenCalledWith(
216
+ 'joinRoom',
217
+ '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4-test_inbox'
218
+ )
219
+ }, 10000)
220
+
221
+ it('Sends a live message', async () => {
222
+ const messageBoxClient = new MessageBoxClient({
223
+ walletClient: mockWalletClient,
224
+ host: 'https://messagebox.babbage.systems',
225
+ enableLogging: true
226
+ })
227
+
228
+ await messageBoxClient.init()
229
+
230
+ const connection = messageBoxClient.initializeConnection()
231
+
232
+ // Simulate WebSocket auth success
233
+ setTimeout(() => {
234
+ socketOnMap.authenticationSuccess?.({ status: 'ok' })
235
+ }, 100)
236
+
237
+ await connection
238
+
239
+ const emitSpy = jest.spyOn(messageBoxClient.testSocket as any, 'emit')
240
+
241
+ // Kick off sending a message (this sets up the ack listener)
242
+ const sendPromise = messageBoxClient.sendLiveMessage({
243
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
244
+ messageBox: 'test_inbox',
245
+ body: 'Test message'
246
+ })
247
+
248
+ // Simulate WebSocket acknowledgment
249
+ setTimeout(() => {
250
+ socketOnMap['sendMessageAck-02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4-test_inbox']?.({
251
+ status: 'success',
252
+ messageId: 'mocked123'
253
+ })
254
+ }, 100)
255
+
256
+ const result = await sendPromise
257
+
258
+ // Check that WebSocket emit happened correctly
259
+ expect(emitSpy).toHaveBeenCalledWith(
260
+ 'sendMessage',
261
+ expect.objectContaining({
262
+ roomId: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4-test_inbox',
263
+ message: expect.objectContaining({
264
+ messageId: '010203',
265
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
266
+ body: expect.stringMatching(/encrypted/)
267
+ })
268
+ })
269
+ )
270
+
271
+ // Check the resolved result
272
+ expect(result).toEqual({
273
+ status: 'success',
274
+ messageId: 'mocked123'
275
+ })
276
+ }, 15000)
277
+
278
+ it('Sends a message', async () => {
279
+ const messageBoxClient = new MessageBoxClient({
280
+ walletClient: mockWalletClient,
281
+ host: 'https://messagebox.babbage.systems',
282
+ enableLogging: true
283
+ })
284
+ await messageBoxClient.init()
285
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
286
+ jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({
287
+ json: async () => ({
288
+ status: 'success',
289
+ message: 'Your message has been sent!'
290
+ }),
291
+ headers: new Headers(),
292
+ ok: true,
293
+ status: 200
294
+ } as unknown as Response)
295
+
296
+ const result = await messageBoxClient.sendMessage({
297
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
298
+ messageBox: 'test_inbox',
299
+ body: { data: 'test' }
300
+ })
301
+
302
+ expect(result).toHaveProperty('message', 'Your message has been sent!')
303
+ })
304
+
305
+ it('Lists available messages', async () => {
306
+ const messageBoxClient = new MessageBoxClient({
307
+ walletClient: mockWalletClient,
308
+ host: 'https://messagebox.babbage.systems',
309
+ enableLogging: true
310
+ })
311
+ await messageBoxClient.init()
312
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
313
+
314
+ jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({
315
+ json: async () => JSON.parse(VALID_LIST_AND_READ_RESULT.body),
316
+ headers: new Headers(),
317
+ ok: true,
318
+ status: 200
319
+ } as unknown as Response)
320
+
321
+ const result = await messageBoxClient.listMessages({ messageBox: 'test_inbox' })
322
+
323
+
324
+ expect(result).toEqual(JSON.parse(VALID_LIST_AND_READ_RESULT.body).messages)
325
+ })
326
+
327
+ it('Acknowledges a message', async () => {
328
+ const messageBoxClient = new MessageBoxClient({
329
+ walletClient: mockWalletClient,
330
+ host: 'https://messagebox.babbage.systems',
331
+ enableLogging: true
332
+ })
333
+ await messageBoxClient.init()
334
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
335
+
336
+ jest.spyOn(messageBoxClient.authFetch, 'fetch').mockResolvedValue({
337
+ json: async () => JSON.parse(VALID_ACK_RESULT.body),
338
+ headers: new Headers(),
339
+ ok: true,
340
+ status: 200
341
+ } as unknown as Response)
342
+
343
+ const result = await messageBoxClient.acknowledgeMessage({ messageIds: ['42'] })
344
+
345
+ expect(result).toEqual(200)
346
+ })
347
+
348
+ it('Throws an error when sendMessage() API fails', async () => {
349
+ const messageBoxClient = new MessageBoxClient({
350
+ walletClient: mockWalletClient,
351
+ host: 'https://messagebox.babbage.systems',
352
+ enableLogging: true
353
+ })
354
+ await messageBoxClient.init()
355
+
356
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
357
+
358
+ jest.spyOn(messageBoxClient.authFetch, 'fetch')
359
+ .mockResolvedValue({
360
+ status: 500,
361
+ statusText: 'Internal Server Error',
362
+ ok: false,
363
+ json: async () => ({ status: 'error', description: 'Internal Server Error' }),
364
+ headers: new Headers()
365
+ } as unknown as Response)
366
+
367
+ await expect(messageBoxClient.sendMessage({
368
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
369
+ messageBox: 'test_inbox',
370
+ body: 'Test Message'
371
+ })).rejects.toThrow('Message sending failed: HTTP 500 - Internal Server Error')
372
+ })
373
+
374
+ it('throws when every host fails', async () => {
375
+ const client = new MessageBoxClient({ walletClient: mockWalletClient, host: 'https://primary', enableLogging: false })
376
+ await client.init()
377
+
378
+ // Pretend there are no advertised replicas
379
+ jest.spyOn(client as any, 'queryAdvertisements').mockResolvedValue([])
380
+
381
+ // Primary host responds with 500
382
+ jest.spyOn(client.authFetch, 'fetch').mockResolvedValue({
383
+ ok: false,
384
+ status: 500,
385
+ statusText: 'Internal Server Error',
386
+ json: async () => ({ status: 'error', description: 'DB down' })
387
+ } as unknown as Response)
388
+
389
+ await expect(client.listMessages({ messageBox: 'inbox' }))
390
+ .rejects.toThrow('Failed to retrieve messages from any host')
391
+ })
392
+
393
+ it('returns [] when at least one host succeeds but has no messages', async () => {
394
+ const client = new MessageBoxClient({ walletClient: mockWalletClient, host: 'https://primary', enableLogging: false })
395
+ await client.init()
396
+
397
+ // One failing replica, one healthy replica
398
+ jest.spyOn(client as any, 'queryAdvertisements').mockResolvedValue([{
399
+ host: 'https://replica'
400
+ }])
401
+
402
+ jest.spyOn(client.authFetch, 'fetch')
403
+ .mockImplementation(async url =>
404
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
405
+ url.startsWith('https://primary')
406
+ ? await Promise.resolve({
407
+ ok: false,
408
+ status: 500,
409
+ statusText: 'Internal Server Error',
410
+ json: async () => ({ status: 'error', description: 'DB down' })
411
+ } as unknown as Response)
412
+ : await Promise.resolve({
413
+ ok: true,
414
+ status: 200,
415
+ json: async () => ({ status: 'success', messages: [] })
416
+ } as unknown as Response)
417
+ )
418
+
419
+ await expect(client.listMessages({ messageBox: 'inbox' })).resolves.toEqual([])
420
+ })
421
+
422
+ it('Throws an error when acknowledgeMessage() API fails', async () => {
423
+ const messageBoxClient = new MessageBoxClient({
424
+ walletClient: mockWalletClient,
425
+ host: 'https://messagebox.babbage.systems',
426
+ enableLogging: true
427
+ })
428
+ await messageBoxClient.init()
429
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
430
+
431
+ jest.spyOn(messageBoxClient.authFetch, 'fetch')
432
+ .mockResolvedValue({
433
+ status: 500,
434
+ json: async () => ({ status: 'error', description: 'Failed to acknowledge messages' })
435
+ } as unknown as Response)
436
+
437
+ await expect(messageBoxClient.acknowledgeMessage({ messageIds: ['42'] }))
438
+ .rejects.toThrow('Failed to acknowledge messages')
439
+ })
440
+
441
+ it('Throws an error when WebSocket is not initialized before listening for messages', async () => {
442
+ const messageBoxClient = new MessageBoxClient({
443
+ walletClient: mockWalletClient,
444
+ host: 'https://messagebox.babbage.systems',
445
+ enableLogging: true
446
+ })
447
+ await messageBoxClient.init()
448
+
449
+ // Stub out the identity key to pass that check
450
+ ; (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4'
451
+
452
+ // Stub out joinRoom to throw like the real one might
453
+ jest.spyOn(messageBoxClient, 'joinRoom').mockRejectedValue(new Error('WebSocket connection not initialized'))
454
+
455
+ await expect(
456
+ messageBoxClient.listenForLiveMessages({
457
+ onMessage: jest.fn(),
458
+ messageBox: 'test_inbox'
459
+ })
460
+ ).rejects.toThrow('WebSocket connection not initialized')
461
+ })
462
+
463
+ it('Emits joinRoom event and listens for incoming messages', async () => {
464
+ const messageBoxClient = new MessageBoxClient({
465
+ walletClient: mockWalletClient,
466
+ host: 'https://messagebox.babbage.systems',
467
+ enableLogging: true
468
+ })
469
+ await messageBoxClient.init()
470
+
471
+ // Mock identity key properly
472
+ jest.spyOn(mockWalletClient, 'getPublicKey').mockResolvedValue({ publicKey: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4' })
473
+
474
+ // Mock socket with `on` method capturing event handlers
475
+ const mockSocket = {
476
+ emit: jest.fn(),
477
+ on: jest.fn()
478
+ } as any
479
+
480
+ // Mock `initializeConnection` so it assigns `socket` & identity key
481
+ jest.spyOn(messageBoxClient, 'initializeConnection').mockImplementation(async () => {
482
+ Object.defineProperty(messageBoxClient, 'testIdentityKey', { get: () => '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4' })
483
+ Object.defineProperty(messageBoxClient, 'testSocket', { get: () => mockSocket });
484
+ (messageBoxClient as any).socket = mockSocket; // Ensures internal socket is set
485
+ (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4' // Ensures identity key is set
486
+ })
487
+
488
+ const onMessageMock = jest.fn()
489
+
490
+ await messageBoxClient.listenForLiveMessages({
491
+ onMessage: onMessageMock,
492
+ messageBox: 'test_inbox'
493
+ })
494
+
495
+ // Ensure `joinRoom` event was emitted with the correct identity key
496
+ expect(mockSocket.emit).toHaveBeenCalledWith('joinRoom', '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4-test_inbox')
497
+
498
+ // Simulate receiving a message
499
+ const receivedMessage = { text: 'Hello, world!' }
500
+
501
+ // Extract & invoke the callback function stored in `on`
502
+ const sendMessageCallback = mockSocket.on.mock.calls.find(
503
+ ([eventName]) => eventName === 'sendMessage-02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4-test_inbox'
504
+ )?.[1] // Extract the callback function
505
+
506
+ if (typeof sendMessageCallback === 'function') {
507
+ sendMessageCallback(receivedMessage)
508
+ }
509
+
510
+ // Ensure `onMessage` was called with the received message
511
+ expect(onMessageMock).toHaveBeenCalledWith(receivedMessage)
512
+ })
513
+
514
+ it('Handles WebSocket connection and disconnection events', async () => {
515
+ const messageBoxClient = new MessageBoxClient({
516
+ walletClient: mockWalletClient,
517
+ host: 'https://messagebox.babbage.systems',
518
+ enableLogging: true
519
+ })
520
+ await messageBoxClient.init()
521
+
522
+ // Simulate identity key
523
+ jest.spyOn(mockWalletClient, 'getPublicKey').mockResolvedValue({ publicKey: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4' })
524
+
525
+ // Simulate connection + disconnection + auth success
526
+ setTimeout(() => {
527
+ socketOnMap.connect?.()
528
+ socketOnMap.disconnect?.()
529
+ socketOnMap.authenticationSuccess?.({ status: 'ok' })
530
+ }, 100)
531
+
532
+ await messageBoxClient.initializeConnection()
533
+
534
+ // Verify event listeners were registered
535
+ expect(mockSocket.on).toHaveBeenCalledWith('connect', expect.any(Function))
536
+ expect(mockSocket.on).toHaveBeenCalledWith('disconnect', expect.any(Function))
537
+ }, 10000)
538
+
539
+ it('throws an error when recipient is empty in sendLiveMessage', async () => {
540
+ const messageBoxClient = new MessageBoxClient({
541
+ walletClient: mockWalletClient,
542
+ host: 'https://messagebox.babbage.systems',
543
+ enableLogging: true
544
+ })
545
+ await messageBoxClient.init()
546
+
547
+ // Mock `initializeConnection` so it assigns `socket` & identity key
548
+ jest.spyOn(messageBoxClient, 'initializeConnection').mockImplementation(async () => {
549
+ Object.defineProperty(messageBoxClient, 'testIdentityKey', { get: () => '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4' })
550
+ Object.defineProperty(messageBoxClient, 'testSocket', { get: () => mockSocket });
551
+ (messageBoxClient as any).socket = mockSocket; // Ensures internal socket is set
552
+ (messageBoxClient as any).myIdentityKey = '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4' // Ensures identity key is set
553
+ })
554
+
555
+ // Mock socket to ensure WebSocket validation does not fail
556
+ const mockSocket = {
557
+ emit: jest.fn()
558
+ } as any
559
+ jest.spyOn(messageBoxClient, 'testSocket', 'get').mockReturnValue(mockSocket)
560
+
561
+ await expect(messageBoxClient.sendLiveMessage({
562
+ recipient: ' ',
563
+ messageBox: 'test_inbox',
564
+ body: 'Test message'
565
+ })).rejects.toThrow('[MB CLIENT ERROR] Recipient identity key is required')
566
+ })
567
+
568
+ it('throws an error when recipient is missing in sendMessage', async () => {
569
+ const messageBoxClient = new MessageBoxClient({
570
+ walletClient: mockWalletClient,
571
+ host: 'https://messagebox.babbage.systems',
572
+ enableLogging: true
573
+ })
574
+ await messageBoxClient.init()
575
+
576
+ await expect(messageBoxClient.sendMessage({
577
+ recipient: '', // Empty recipient
578
+ messageBox: 'test_inbox',
579
+ body: 'Test message'
580
+ })).rejects.toThrow('You must provide a message recipient!')
581
+
582
+ await expect(messageBoxClient.sendMessage({
583
+ recipient: ' ', // Whitespace recipient
584
+ messageBox: 'test_inbox',
585
+ body: 'Test message'
586
+ })).rejects.toThrow('You must provide a message recipient!')
587
+
588
+ await expect(messageBoxClient.sendMessage({
589
+ recipient: null as any, // Null recipient
590
+ messageBox: 'test_inbox',
591
+ body: 'Test message'
592
+ })).rejects.toThrow('You must provide a message recipient!')
593
+ })
594
+
595
+ it('throws an error when messageBox is missing in sendMessage', async () => {
596
+ const messageBoxClient = new MessageBoxClient({
597
+ walletClient: mockWalletClient,
598
+ host: 'https://messagebox.babbage.systems',
599
+ enableLogging: true
600
+ })
601
+ await messageBoxClient.init()
602
+
603
+ await expect(messageBoxClient.sendMessage({
604
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
605
+ messageBox: '', // Empty messageBox
606
+ body: 'Test message'
607
+ })).rejects.toThrow('You must provide a messageBox to send this message into!')
608
+
609
+ await expect(messageBoxClient.sendMessage({
610
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
611
+ messageBox: ' ', // Whitespace messageBox
612
+ body: 'Test message'
613
+ })).rejects.toThrow('You must provide a messageBox to send this message into!')
614
+
615
+ await expect(messageBoxClient.sendMessage({
616
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
617
+ messageBox: null as any, // Null messageBox
618
+ body: 'Test message'
619
+ })).rejects.toThrow('You must provide a messageBox to send this message into!')
620
+ })
621
+
622
+ it('throws an error when message body is missing in sendMessage', async () => {
623
+ const messageBoxClient = new MessageBoxClient({
624
+ walletClient: mockWalletClient,
625
+ host: 'https://messagebox.babbage.systems',
626
+ enableLogging: true
627
+ })
628
+ await messageBoxClient.init()
629
+
630
+ await expect(messageBoxClient.sendMessage({
631
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
632
+ messageBox: 'test_inbox',
633
+ body: '' // Empty body
634
+ })).rejects.toThrow('Every message must have a body!')
635
+
636
+ await expect(messageBoxClient.sendMessage({
637
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
638
+ messageBox: 'test_inbox',
639
+ body: ' ' // Whitespace body
640
+ })).rejects.toThrow('Every message must have a body!')
641
+
642
+ await expect(messageBoxClient.sendMessage({
643
+ recipient: '02b463b8ef7f03c47fba2679c7334d13e4939b8ca30dbb6bbd22e34ea3e9b1b0e4',
644
+ messageBox: 'test_inbox',
645
+ body: null as any // Null body
646
+ })).rejects.toThrow('Every message must have a body!')
647
+ })
648
+
649
+ it('throws an error when messageBox is empty in listMessages', async () => {
650
+ const messageBoxClient = new MessageBoxClient({
651
+ walletClient: mockWalletClient,
652
+ host: 'https://messagebox.babbage.systems',
653
+ enableLogging: true
654
+ })
655
+ await messageBoxClient.init()
656
+
657
+ await expect(messageBoxClient.listMessages({
658
+ messageBox: '' // Empty messageBox
659
+ })).rejects.toThrow('MessageBox cannot be empty')
660
+
661
+ await expect(messageBoxClient.listMessages({
662
+ messageBox: ' ' // Whitespace messageBox
663
+ })).rejects.toThrow('MessageBox cannot be empty')
664
+ })
665
+
666
+ it('throws an error when messageIds is empty in acknowledgeMessage', async () => {
667
+ const messageBoxClient = new MessageBoxClient({
668
+ walletClient: mockWalletClient,
669
+ host: 'https://messagebox.babbage.systems',
670
+ enableLogging: true
671
+ })
672
+ await messageBoxClient.init()
673
+
674
+ await expect(messageBoxClient.acknowledgeMessage({
675
+ messageIds: [] // Empty array
676
+ })).rejects.toThrow('Message IDs array cannot be empty')
677
+
678
+ await expect(messageBoxClient.acknowledgeMessage({
679
+ messageIds: undefined as any // Undefined value
680
+ })).rejects.toThrow('Message IDs array cannot be empty')
681
+
682
+ await expect(messageBoxClient.acknowledgeMessage({
683
+ messageIds: null as any // Null value
684
+ })).rejects.toThrow('Message IDs array cannot be empty')
685
+
686
+ await expect(messageBoxClient.acknowledgeMessage({
687
+ messageIds: 'invalid' as any // Not an array
688
+ })).rejects.toThrow('Message IDs array cannot be empty')
689
+ })
690
+
691
+ it('Uses default host if none is provided', () => {
692
+ const client = new MessageBoxClient({ walletClient: mockWalletClient })
693
+
694
+ expect((client as any).initialized).toBe(false)
695
+ expect((client as any).host).toBe('https://messagebox.babbage.systems')
696
+ })
697
+
698
+ it('Calls init() to set up a default host when missing', async () => {
699
+ const client = new MessageBoxClient({ walletClient: mockWalletClient })
700
+
701
+ expect((client as any).initialized).toBe(false)
702
+
703
+ await client.init()
704
+
705
+ expect((client as any).initialized).toBe(true)
706
+ expect(typeof (client as any).host).toBe('string')
707
+ expect((client as any).host.length).toBeGreaterThan(0)
708
+ })
709
+
710
+ it('init() overrides host if passed with override=true', async () => {
711
+ const client = new MessageBoxClient({
712
+ walletClient: mockWalletClient,
713
+ host: 'https://original-host.example'
714
+ })
715
+
716
+ expect((client as any).initialized).toBe(false)
717
+ expect((client as any).host).toBe('https://original-host.example')
718
+
719
+ await client.init('https://new-host.example')
720
+
721
+ expect((client as any).initialized).toBe(true)
722
+ expect((client as any).host).toBe('https://new-host.example')
723
+ })
724
+
725
+ it('does not anoint when advert already exists', async () => {
726
+ jest.spyOn(MessageBoxClient.prototype as any, 'queryAdvertisements').mockResolvedValue([{
727
+ host: 'https://messagebox.babbage.systems'
728
+ }])
729
+ const spy = jest.spyOn(MessageBoxClient.prototype as any, 'anointHost')
730
+ await new MessageBoxClient({ walletClient: mockWalletClient }).init()
731
+ expect(spy).not.toHaveBeenCalled()
732
+ })
733
+
734
+ it('resolveHostForRecipient returns the first advertised host', async () => {
735
+ const client = new MessageBoxClient({
736
+ walletClient: mockWalletClient,
737
+ host: 'https://default.box'
738
+ })
739
+ await client.init()
740
+
741
+ // For this ONE call return two adverts – the first is selected
742
+ ; (MessageBoxClient.prototype as any).queryAdvertisements
743
+ .mockResolvedValueOnce([
744
+ { host: 'https://peer.box' }, { host: 'https://second.box' }])
745
+
746
+ const result = await client.resolveHostForRecipient('02aa…deadbeef')
747
+ expect(result).toBe('https://peer.box')
748
+ })
749
+
750
+ it('resolveHostForRecipient falls back to this.host when no adverts exist', async () => {
751
+ const client = new MessageBoxClient({
752
+ walletClient: mockWalletClient,
753
+ host: 'https://default.box'
754
+ })
755
+ await client.init()
756
+ ; (MessageBoxClient.prototype as any).queryAdvertisements
757
+ .mockResolvedValueOnce([])
758
+
759
+ const result = await client.resolveHostForRecipient('03bb…cafef00d')
760
+
761
+ expect(result).toBe('https://default.box')
762
+ })
763
+ })