@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.
- package/LICENSE.txt +28 -0
- package/README.md +1013 -0
- package/mod.ts +3 -0
- package/package.json +80 -0
- package/src/MessageBoxClient.ts +1341 -0
- package/src/PeerPayClient.ts +386 -0
- package/src/Utils/logger.ts +27 -0
- package/src/__tests/MessageBoxClient.test.ts +763 -0
- package/src/__tests/PeerPayClientUnit.test.ts +245 -0
- package/src/__tests/integration/integrationEncrypted.test.ts +103 -0
- package/src/__tests/integration/integrationHTTP.test.ts +158 -0
- package/src/__tests/integration/integrationOverlay.test.ts +163 -0
- package/src/__tests/integration/integrationWS.test.ts +147 -0
- package/src/__tests/integration/testServer.ts +68 -0
- package/src/types.ts +108 -0
|
@@ -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
|
+
})
|