@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,245 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
import { PeerPayClient } from '../PeerPayClient.js'
|
|
3
|
+
import { WalletClient, CreateHmacResult, PrivateKey } from '@bsv/sdk'
|
|
4
|
+
import { jest } from '@jest/globals'
|
|
5
|
+
|
|
6
|
+
const toArray = (msg: any, enc?: 'hex' | 'utf8' | 'base64'): any[] => {
|
|
7
|
+
if (Array.isArray(msg)) return msg.slice()
|
|
8
|
+
if (msg === undefined) return []
|
|
9
|
+
|
|
10
|
+
if (typeof msg !== 'string') {
|
|
11
|
+
return Array.from(msg, (item: any) => item | 0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
switch (enc) {
|
|
15
|
+
case 'hex': {
|
|
16
|
+
const matches = msg.match(/.{1,2}/g)
|
|
17
|
+
return matches != null ? matches.map(byte => parseInt(byte, 16)) : []
|
|
18
|
+
}
|
|
19
|
+
case 'base64':
|
|
20
|
+
return Array.from(Buffer.from(msg, 'base64'))
|
|
21
|
+
default:
|
|
22
|
+
return Array.from(Buffer.from(msg, 'utf8'))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Mock dependencies
|
|
27
|
+
jest.mock('@bsv/sdk', () => {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
29
|
+
const actualSDK = jest.requireActual('@bsv/sdk') as any
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...actualSDK,
|
|
33
|
+
WalletClient: jest.fn().mockImplementation(() => ({
|
|
34
|
+
getPublicKey: jest.fn(),
|
|
35
|
+
createAction: jest.fn(),
|
|
36
|
+
internalizeAction: jest.fn(),
|
|
37
|
+
createHmac: jest.fn<() => Promise<CreateHmacResult>>().mockResolvedValue({
|
|
38
|
+
hmac: [1, 2, 3, 4, 5]
|
|
39
|
+
})
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('PeerPayClient Unit Tests', () => {
|
|
45
|
+
let peerPayClient: PeerPayClient
|
|
46
|
+
let mockWalletClient: jest.Mocked<WalletClient>
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
jest.clearAllMocks()
|
|
50
|
+
|
|
51
|
+
mockWalletClient = new WalletClient() as jest.Mocked<WalletClient>
|
|
52
|
+
|
|
53
|
+
// Ensure a valid compressed public key (33 bytes, hex format)
|
|
54
|
+
mockWalletClient.getPublicKey.mockResolvedValue({
|
|
55
|
+
publicKey: PrivateKey.fromRandom().toPublicKey().toString()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
mockWalletClient.createAction.mockResolvedValue({
|
|
59
|
+
tx: toArray('mockedTransaction', 'utf8')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
peerPayClient = new PeerPayClient({
|
|
63
|
+
messageBoxHost: 'https://messagebox.babbage.systems',
|
|
64
|
+
walletClient: mockWalletClient
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('createPaymentToken', () => {
|
|
69
|
+
it('should create a valid payment token', async () => {
|
|
70
|
+
mockWalletClient.getPublicKey.mockResolvedValue({
|
|
71
|
+
publicKey: PrivateKey.fromRandom().toPublicKey().toString()
|
|
72
|
+
})
|
|
73
|
+
mockWalletClient.createAction.mockResolvedValue({ tx: toArray('mockedTransaction', 'utf8') })
|
|
74
|
+
|
|
75
|
+
const payment = { recipient: PrivateKey.fromRandom().toPublicKey().toString(), amount: 5 }
|
|
76
|
+
const token = await peerPayClient.createPaymentToken(payment)
|
|
77
|
+
|
|
78
|
+
expect(token).toHaveProperty('amount', 5)
|
|
79
|
+
expect(mockWalletClient.getPublicKey).toHaveBeenCalledWith(expect.any(Object))
|
|
80
|
+
expect(mockWalletClient.createAction).toHaveBeenCalledWith(expect.any(Object))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should throw an error if recipient public key cannot be derived', async () => {
|
|
84
|
+
mockWalletClient.getPublicKey.mockResolvedValue({ publicKey: '' }) // Empty key
|
|
85
|
+
|
|
86
|
+
await expect(peerPayClient.createPaymentToken({ recipient: 'invalid', amount: 5 }))
|
|
87
|
+
.rejects.toThrow('Failed to derive recipient’s public key')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should throw an error if amount is <= 0', async () => {
|
|
91
|
+
(mockWalletClient.getPublicKey as jest.MockedFunction<typeof mockWalletClient.getPublicKey>)
|
|
92
|
+
.mockResolvedValue({
|
|
93
|
+
publicKey: PrivateKey.fromRandom().toPublicKey().toString()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
await expect(peerPayClient.createPaymentToken({
|
|
97
|
+
recipient: PrivateKey.fromRandom().toPublicKey().toString(),
|
|
98
|
+
amount: 0
|
|
99
|
+
}))
|
|
100
|
+
.rejects.toThrow('Invalid payment details: recipient and valid amount are required')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Test: sendPayment
|
|
105
|
+
describe('sendPayment', () => {
|
|
106
|
+
it('should call sendMessage with valid payment', async () => {
|
|
107
|
+
const sendMessageSpy = jest.spyOn(peerPayClient, 'sendMessage').mockResolvedValue({
|
|
108
|
+
status: 'success',
|
|
109
|
+
messageId: 'mockedMessageId'
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const payment = { recipient: 'recipientKey', amount: 3 }
|
|
113
|
+
|
|
114
|
+
console.log('[TEST] Calling sendPayment...')
|
|
115
|
+
await peerPayClient.sendPayment(payment)
|
|
116
|
+
console.log('[TEST] sendPayment finished.')
|
|
117
|
+
|
|
118
|
+
expect(sendMessageSpy).toHaveBeenCalledWith({
|
|
119
|
+
recipient: 'recipientKey',
|
|
120
|
+
messageBox: 'payment_inbox',
|
|
121
|
+
body: expect.any(String)
|
|
122
|
+
})
|
|
123
|
+
}, 10000)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Test: sendLivePayment
|
|
127
|
+
describe('sendLivePayment', () => {
|
|
128
|
+
it('should call createPaymentToken and sendLiveMessage with correct parameters', async () => {
|
|
129
|
+
jest.spyOn(peerPayClient, 'createPaymentToken').mockResolvedValue({
|
|
130
|
+
customInstructions: {
|
|
131
|
+
derivationPrefix: 'prefix',
|
|
132
|
+
derivationSuffix: 'suffix'
|
|
133
|
+
},
|
|
134
|
+
transaction: Array.from(new Uint8Array([1, 2, 3, 4, 5])),
|
|
135
|
+
amount: 2
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
jest.spyOn(peerPayClient, 'sendLiveMessage').mockResolvedValue({
|
|
139
|
+
status: 'success',
|
|
140
|
+
messageId: 'mockedMessageId'
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const payment = { recipient: 'recipientKey', amount: 2 }
|
|
144
|
+
await peerPayClient.sendLivePayment(payment)
|
|
145
|
+
|
|
146
|
+
expect(peerPayClient.createPaymentToken).toHaveBeenCalledWith(payment)
|
|
147
|
+
expect(peerPayClient.sendLiveMessage).toHaveBeenCalledWith({
|
|
148
|
+
recipient: 'recipientKey',
|
|
149
|
+
messageBox: 'payment_inbox',
|
|
150
|
+
body: "{\"customInstructions\":{\"derivationPrefix\":\"prefix\",\"derivationSuffix\":\"suffix\"},\"transaction\":[1,2,3,4,5],\"amount\":2}"
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Test: acceptPayment
|
|
156
|
+
describe('acceptPayment', () => {
|
|
157
|
+
it('should call internalizeAction and acknowledgeMessage', async () => {
|
|
158
|
+
mockWalletClient.internalizeAction.mockResolvedValue({ accepted: true })
|
|
159
|
+
jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('acknowledged')
|
|
160
|
+
|
|
161
|
+
const payment = {
|
|
162
|
+
messageId: '123',
|
|
163
|
+
sender: 'senderKey',
|
|
164
|
+
token: {
|
|
165
|
+
customInstructions: { derivationPrefix: 'prefix', derivationSuffix: 'suffix' },
|
|
166
|
+
transaction: toArray('mockedTransaction', 'utf8'),
|
|
167
|
+
amount: 6
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await peerPayClient.acceptPayment(payment)
|
|
172
|
+
|
|
173
|
+
expect(mockWalletClient.internalizeAction).toHaveBeenCalled()
|
|
174
|
+
expect(peerPayClient.acknowledgeMessage).toHaveBeenCalledWith({ messageIds: ['123'] })
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Test: rejectPayment
|
|
179
|
+
describe('rejectPayment', () => {
|
|
180
|
+
it('should refund payment minus fee', async () => {
|
|
181
|
+
jest.spyOn(peerPayClient, 'acceptPayment').mockResolvedValue(undefined)
|
|
182
|
+
jest.spyOn(peerPayClient, 'sendPayment').mockResolvedValue(undefined)
|
|
183
|
+
jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('acknowledged')
|
|
184
|
+
|
|
185
|
+
const payment = {
|
|
186
|
+
messageId: '123',
|
|
187
|
+
sender: 'senderKey',
|
|
188
|
+
token: {
|
|
189
|
+
customInstructions: { derivationPrefix: 'prefix', derivationSuffix: 'suffix' },
|
|
190
|
+
transaction: toArray('mockedTransaction', 'utf8'),
|
|
191
|
+
amount: 2000
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await peerPayClient.rejectPayment(payment)
|
|
196
|
+
|
|
197
|
+
expect(peerPayClient.acceptPayment).toHaveBeenCalledWith(payment)
|
|
198
|
+
expect(peerPayClient.sendPayment).toHaveBeenCalledWith({
|
|
199
|
+
recipient: 'senderKey',
|
|
200
|
+
amount: 1000 // Deduct satoshi fee
|
|
201
|
+
})
|
|
202
|
+
expect(peerPayClient.acknowledgeMessage).toHaveBeenCalledWith({
|
|
203
|
+
messageIds: ['123']
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Test: listIncomingPayments
|
|
209
|
+
describe('listIncomingPayments', () => {
|
|
210
|
+
it('should return parsed payment messages', async () => {
|
|
211
|
+
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
|
|
212
|
+
{
|
|
213
|
+
messageId: '1',
|
|
214
|
+
sender: 'sender1',
|
|
215
|
+
created_at: '2025-03-05T12:00:00Z',
|
|
216
|
+
updated_at: '2025-03-05T12:05:00Z',
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
customInstructions: { derivationPrefix: 'prefix1', derivationSuffix: 'suffix1' },
|
|
219
|
+
transaction: toArray('mockedTransaction1', 'utf8'),
|
|
220
|
+
amount: 3
|
|
221
|
+
})
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
messageId: '2',
|
|
225
|
+
sender: 'sender2',
|
|
226
|
+
created_at: '2025-03-05T12:10:00Z',
|
|
227
|
+
updated_at: '2025-03-05T12:15:00Z',
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
customInstructions: { derivationPrefix: 'prefix2', derivationSuffix: 'suffix2' },
|
|
230
|
+
transaction: toArray('mockedTransaction2', 'utf8'),
|
|
231
|
+
amount: 9
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
])
|
|
235
|
+
|
|
236
|
+
const payments = await peerPayClient.listIncomingPayments()
|
|
237
|
+
|
|
238
|
+
expect(payments).toHaveLength(2)
|
|
239
|
+
expect(payments[0]).toHaveProperty('sender', 'sender1')
|
|
240
|
+
expect(payments[0].token.amount).toBe(3)
|
|
241
|
+
expect(payments[1]).toHaveProperty('sender', 'sender2')
|
|
242
|
+
expect(payments[1].token.amount).toBe(9)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
import { MessageBoxClient } from '../../MessageBoxClient.js'
|
|
3
|
+
import { WalletClient } from '@bsv/sdk'
|
|
4
|
+
import { webcrypto } from 'crypto'
|
|
5
|
+
import { expect, test, describe, beforeAll } from '@jest/globals'
|
|
6
|
+
|
|
7
|
+
(global as any).self = { crypto: webcrypto }
|
|
8
|
+
|
|
9
|
+
jest.setTimeout(20000)
|
|
10
|
+
|
|
11
|
+
const walletClient = new WalletClient('json-api', 'https://messagebox.babbage.systems')
|
|
12
|
+
const messageBoxClient = new MessageBoxClient({
|
|
13
|
+
host: 'https://messagebox.babbage.systems',
|
|
14
|
+
walletClient,
|
|
15
|
+
enableLogging: true,
|
|
16
|
+
networkPreset: 'local'
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
let identityKey: string
|
|
20
|
+
|
|
21
|
+
describe('Encryption Integration Tests', () => {
|
|
22
|
+
const messageBox = 'testBox'
|
|
23
|
+
const plaintext = 'This is a secure test message.'
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
const result = await walletClient.getPublicKey({ identityKey: true })
|
|
27
|
+
identityKey = result.publicKey
|
|
28
|
+
|
|
29
|
+
await messageBoxClient.initializeConnection()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('encrypts and decrypts a message to self successfully', async () => {
|
|
33
|
+
// Encrypt
|
|
34
|
+
const { ciphertext } = await walletClient.encrypt({
|
|
35
|
+
plaintext: Array.from(new TextEncoder().encode(plaintext)),
|
|
36
|
+
protocolID: [1, 'messagebox'],
|
|
37
|
+
keyID: '1',
|
|
38
|
+
counterparty: 'self'
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(Array.isArray(ciphertext)).toBe(true)
|
|
42
|
+
expect(ciphertext.length).toBeGreaterThan(0)
|
|
43
|
+
|
|
44
|
+
// Decrypt
|
|
45
|
+
const { plaintext: decryptedBytes } = await walletClient.decrypt({
|
|
46
|
+
ciphertext,
|
|
47
|
+
protocolID: [1, 'messagebox'],
|
|
48
|
+
keyID: '1',
|
|
49
|
+
counterparty: 'self'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const decrypted = new TextDecoder().decode(Uint8Array.from(decryptedBytes))
|
|
53
|
+
expect(decrypted).toBe(plaintext)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('sends and receives encrypted message using MessageBoxClient', async () => {
|
|
57
|
+
// Send message to self
|
|
58
|
+
const sendResult = await messageBoxClient.sendMessage({
|
|
59
|
+
recipient: identityKey,
|
|
60
|
+
messageBox,
|
|
61
|
+
body: plaintext
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(sendResult.status).toBe('success')
|
|
65
|
+
expect(typeof sendResult.messageId).toBe('string')
|
|
66
|
+
|
|
67
|
+
// List and decrypt
|
|
68
|
+
const messages = await messageBoxClient.listMessages({ messageBox })
|
|
69
|
+
const last = messages.at(-1)
|
|
70
|
+
|
|
71
|
+
expect(last).toBeDefined()
|
|
72
|
+
expect(last?.body).toBe(plaintext)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('encrypted message is not stored or transmitted as plaintext', async () => {
|
|
76
|
+
// Send encrypted message to self
|
|
77
|
+
const sendResult = await messageBoxClient.sendMessage({
|
|
78
|
+
recipient: identityKey,
|
|
79
|
+
messageBox,
|
|
80
|
+
body: plaintext
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(sendResult.status).toBe('success')
|
|
84
|
+
|
|
85
|
+
// Manually fetch raw HTTP response
|
|
86
|
+
const fetch = await messageBoxClient.authFetch.fetch(
|
|
87
|
+
'https://messagebox.babbage.systems/listMessages',
|
|
88
|
+
{
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ messageBox })
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const raw = await fetch.json()
|
|
96
|
+
const rawBody = raw.messages.at(-1)?.body
|
|
97
|
+
|
|
98
|
+
expect(typeof rawBody).toBe('string')
|
|
99
|
+
const parsed = JSON.parse(rawBody)
|
|
100
|
+
expect(typeof parsed.encryptedMessage).toBe('string')
|
|
101
|
+
expect(parsed.encryptedMessage.includes(plaintext)).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
import { MessageBoxClient } from '../../MessageBoxClient.js'
|
|
3
|
+
import { WalletClient } from '@bsv/sdk'
|
|
4
|
+
import { webcrypto } from 'crypto'
|
|
5
|
+
|
|
6
|
+
// Ensure Jest doesn't mock WalletClient
|
|
7
|
+
jest.unmock('@bsv/sdk');
|
|
8
|
+
|
|
9
|
+
(global as any).self = { crypto: webcrypto }
|
|
10
|
+
|
|
11
|
+
jest.setTimeout(20000)
|
|
12
|
+
|
|
13
|
+
// Explicitly initialize WalletClient with Meta Net Client (MNC)
|
|
14
|
+
const walletClient = new WalletClient('json-api', 'localhost')
|
|
15
|
+
|
|
16
|
+
// Initialize MessageBoxClient for HTTP-Only Testing
|
|
17
|
+
const messageBoxClient = new MessageBoxClient({
|
|
18
|
+
host: 'https://messagebox.babbage.systems',
|
|
19
|
+
walletClient
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('MessageBoxClient HTTP Integration Tests (No WebSocket)', () => {
|
|
23
|
+
let recipientKey: string
|
|
24
|
+
let testMessageId: string
|
|
25
|
+
const messageBox = 'testBox'
|
|
26
|
+
const testMessage = 'Hello, this is an integration test.'
|
|
27
|
+
const testMessage2 = 'Another test message to avoid duplicates.'
|
|
28
|
+
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
try {
|
|
31
|
+
console.log('[DEBUG] Retrieving public key...')
|
|
32
|
+
const publicKeyResponse = await walletClient.getPublicKey({ identityKey: true })
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
publicKeyResponse?.publicKey == null ||
|
|
36
|
+
typeof publicKeyResponse.publicKey !== 'string' ||
|
|
37
|
+
publicKeyResponse.publicKey.trim() === ''
|
|
38
|
+
) {
|
|
39
|
+
throw new Error('[ERROR] getPublicKey returned an invalid key!')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
recipientKey = publicKeyResponse.publicKey.trim()
|
|
43
|
+
console.log('[DEBUG] Successfully assigned recipientKey:', recipientKey)
|
|
44
|
+
|
|
45
|
+
// Ensure identity key is set internally in MessageBoxClient
|
|
46
|
+
await messageBoxClient.initializeConnection()
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('[ERROR] Failed to set up test:', error)
|
|
49
|
+
throw error
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
afterAll(async () => {
|
|
54
|
+
try {
|
|
55
|
+
if (testMessageId !== undefined && testMessageId !== '') {
|
|
56
|
+
console.log('[DEBUG] Cleaning up test messages...')
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('[ERROR] Failed to acknowledge test message:', error)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** TEST 1: Send a Message with Correct Payment **/
|
|
64
|
+
test('should send a message successfully with correct payment', async () => {
|
|
65
|
+
const response = await messageBoxClient.sendMessage({
|
|
66
|
+
recipient: recipientKey,
|
|
67
|
+
messageBox,
|
|
68
|
+
body: testMessage,
|
|
69
|
+
skipEncryption: true // TEMPORARY to test if this fixes the 400
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
console.log('[DEBUG] SendMessage Response:', response)
|
|
73
|
+
|
|
74
|
+
expect(response).toHaveProperty('status', 'success')
|
|
75
|
+
expect(response).toHaveProperty('messageId', expect.any(String))
|
|
76
|
+
|
|
77
|
+
testMessageId = response.messageId // Store for cleanup
|
|
78
|
+
}, 30000)
|
|
79
|
+
|
|
80
|
+
/** TEST 2: List Messages **/
|
|
81
|
+
test('should list messages from messageBox', async () => {
|
|
82
|
+
const messages = await messageBoxClient.listMessages({ messageBox })
|
|
83
|
+
expect(messages.length).toBeGreaterThan(0)
|
|
84
|
+
expect(messages.some(msg => msg.body === testMessage)).toBe(true)
|
|
85
|
+
}, 15000)
|
|
86
|
+
|
|
87
|
+
/** TEST 3: List Messages from an Empty MessageBox **/
|
|
88
|
+
test('should return an empty list if no messages exist', async () => {
|
|
89
|
+
const messages = await messageBoxClient.listMessages({ messageBox: 'emptyBox' })
|
|
90
|
+
expect(messages).toEqual([])
|
|
91
|
+
}, 15000)
|
|
92
|
+
|
|
93
|
+
/** TEST 4: Acknowledge a Message **/
|
|
94
|
+
test('should acknowledge (delete) a message', async () => {
|
|
95
|
+
const ackResponse = await messageBoxClient.acknowledgeMessage({ messageIds: [testMessageId] })
|
|
96
|
+
expect(ackResponse).toBe('success')
|
|
97
|
+
}, 15000)
|
|
98
|
+
|
|
99
|
+
/** TEST 5: Acknowledge a Nonexistent Message **/
|
|
100
|
+
test('should fail to acknowledge a nonexistent message', async () => {
|
|
101
|
+
await expect(
|
|
102
|
+
messageBoxClient.acknowledgeMessage({ messageIds: ['fakeMessageId'] })
|
|
103
|
+
).rejects.toThrow('Message not found!')
|
|
104
|
+
}, 15000)
|
|
105
|
+
|
|
106
|
+
/** TEST 6: Send Message with Invalid Recipient **/
|
|
107
|
+
test('should fail if recipient is invalid', async () => {
|
|
108
|
+
await expect(
|
|
109
|
+
messageBoxClient.sendMessage({
|
|
110
|
+
recipient: '', // Invalid recipient
|
|
111
|
+
messageBox,
|
|
112
|
+
body: testMessage
|
|
113
|
+
})
|
|
114
|
+
).rejects.toThrow('You must provide a message recipient!')
|
|
115
|
+
}, 15000)
|
|
116
|
+
|
|
117
|
+
/** TEST 7: Send Message with Empty Body **/
|
|
118
|
+
test('should fail if message body is empty', async () => {
|
|
119
|
+
await expect(
|
|
120
|
+
messageBoxClient.sendMessage({
|
|
121
|
+
recipient: recipientKey,
|
|
122
|
+
messageBox,
|
|
123
|
+
body: '' // Empty message
|
|
124
|
+
})
|
|
125
|
+
).rejects.toThrow('Every message must have a body!')
|
|
126
|
+
}, 15000)
|
|
127
|
+
|
|
128
|
+
/** TEST 8: Send Message with Excessive Payment (Should still succeed) **/
|
|
129
|
+
test('should send a message even if payment is more than required', async () => {
|
|
130
|
+
const response = await messageBoxClient.sendMessage({
|
|
131
|
+
recipient: recipientKey,
|
|
132
|
+
messageBox,
|
|
133
|
+
body: testMessage2,
|
|
134
|
+
skipEncryption: true
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
console.log('[DEBUG] Overpayment SendMessage Response:', response)
|
|
138
|
+
|
|
139
|
+
expect(response.status).toBe('success')
|
|
140
|
+
}, 15000)
|
|
141
|
+
|
|
142
|
+
/** TEST: Send a message without encryption **/
|
|
143
|
+
test('should send a message without encryption when skipEncryption is true', async () => {
|
|
144
|
+
const plaintextMessage = 'Unencrypted test message'
|
|
145
|
+
const response = await messageBoxClient.sendMessage({
|
|
146
|
+
recipient: recipientKey,
|
|
147
|
+
messageBox,
|
|
148
|
+
body: plaintextMessage,
|
|
149
|
+
skipEncryption: true // Bypass encryption
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(response).toHaveProperty('status', 'success')
|
|
153
|
+
expect(response).toHaveProperty('messageId', expect.any(String))
|
|
154
|
+
|
|
155
|
+
const messages = await messageBoxClient.listMessages({ messageBox })
|
|
156
|
+
expect(messages.some(msg => msg.body === plaintextMessage)).toBe(true)
|
|
157
|
+
}, 30000)
|
|
158
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
import { MessageBoxClient } from '../../MessageBoxClient.js'
|
|
3
|
+
import { WalletClient } from '@bsv/sdk'
|
|
4
|
+
import { webcrypto } from 'crypto'
|
|
5
|
+
|
|
6
|
+
(global as any).self = { crypto: webcrypto }
|
|
7
|
+
|
|
8
|
+
jest.setTimeout(20000)
|
|
9
|
+
|
|
10
|
+
const MESSAGEBOX_HOST = 'http://localhost:5001'
|
|
11
|
+
|
|
12
|
+
const walletA = new WalletClient('json-api', 'localhost')
|
|
13
|
+
const walletB = new WalletClient('json-api', 'localhost')
|
|
14
|
+
|
|
15
|
+
const clientA = new MessageBoxClient({
|
|
16
|
+
host: MESSAGEBOX_HOST,
|
|
17
|
+
walletClient: walletA,
|
|
18
|
+
networkPreset: 'local',
|
|
19
|
+
enableLogging: true
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const clientB = new MessageBoxClient({
|
|
23
|
+
host: MESSAGEBOX_HOST,
|
|
24
|
+
walletClient: walletB,
|
|
25
|
+
networkPreset: 'local',
|
|
26
|
+
enableLogging: true
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
let identityKeyA: string
|
|
30
|
+
let identityKeyB: string
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
identityKeyA = (await walletA.getPublicKey({ identityKey: true })).publicKey
|
|
34
|
+
identityKeyB = (await walletB.getPublicKey({ identityKey: true })).publicKey
|
|
35
|
+
await clientA.initializeConnection()
|
|
36
|
+
await clientB.initializeConnection()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
afterAll(async () => {
|
|
40
|
+
await clientA.disconnectWebSocket()
|
|
41
|
+
await clientB.disconnectWebSocket()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('Overlay Integration Tests', () => {
|
|
45
|
+
const selfBox = 'overlay_self_box'
|
|
46
|
+
const peerBox = 'forwarded_overlay_box'
|
|
47
|
+
|
|
48
|
+
test('clientA broadcasts overlay advertisement', async () => {
|
|
49
|
+
const result = await clientA.anointHost(MESSAGEBOX_HOST)
|
|
50
|
+
expect(result).toHaveProperty('txid')
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('clientA resolves own host via overlay', async () => {
|
|
55
|
+
const resolved = await (clientA as any).resolveHostForRecipient(identityKeyA)
|
|
56
|
+
console.log('[TEST] Resolved host:', resolved)
|
|
57
|
+
expect(resolved).toBe(MESSAGEBOX_HOST)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('clientA sends message to self via overlay', async () => {
|
|
61
|
+
const response = await clientA.sendMessage({
|
|
62
|
+
recipient: identityKeyA,
|
|
63
|
+
messageBox: selfBox,
|
|
64
|
+
body: 'hello via overlay'
|
|
65
|
+
})
|
|
66
|
+
expect(response.status).toBe('success')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('clientA lists self messages via overlay', async () => {
|
|
70
|
+
const messages = await clientA.listMessages({ messageBox: selfBox })
|
|
71
|
+
expect(messages.length).toBeGreaterThan(0)
|
|
72
|
+
expect(messages.at(-1)?.body).toContain('hello via overlay')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('clientA acknowledges self messages via overlay', async () => {
|
|
76
|
+
const messages = await clientA.listMessages({ messageBox: selfBox })
|
|
77
|
+
const ids = messages.map(m => m.messageId).filter(Boolean)
|
|
78
|
+
expect(ids.length).toBeGreaterThan(0)
|
|
79
|
+
const status = await clientA.acknowledgeMessage({ messageIds: ids })
|
|
80
|
+
expect(status).toBe('success')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('clientB broadcasts overlay advertisement', async () => {
|
|
84
|
+
const result = await clientB.anointHost(MESSAGEBOX_HOST)
|
|
85
|
+
expect(result).toHaveProperty('txid')
|
|
86
|
+
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('clientA sends message to clientB via overlay', async () => {
|
|
90
|
+
const response = await clientA.sendMessage({
|
|
91
|
+
recipient: identityKeyB,
|
|
92
|
+
messageBox: peerBox,
|
|
93
|
+
body: 'delivered to peer via overlay'
|
|
94
|
+
})
|
|
95
|
+
expect(response.status).toBe('success')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('clientB receives overlay message from clientA', async () => {
|
|
99
|
+
const messages = await clientB.listMessages({ messageBox: peerBox })
|
|
100
|
+
expect(messages.some(m => m.body.includes('delivered to peer via overlay'))).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('clientB acknowledges overlay message from clientA', async () => {
|
|
104
|
+
const messages = await clientB.listMessages({ messageBox: peerBox })
|
|
105
|
+
const ids = messages.map(m => m.messageId).filter(Boolean)
|
|
106
|
+
const result = await clientB.acknowledgeMessage({ messageIds: ids })
|
|
107
|
+
expect(result).toBe('success')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('clientA verifies clientB host resolution', async () => {
|
|
111
|
+
const resolved = await (clientA as any).resolveHostForRecipient(identityKeyB)
|
|
112
|
+
expect(resolved).toBe(MESSAGEBOX_HOST)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('overlay advertisement is idempotent', async () => {
|
|
116
|
+
const result1 = await clientA.anointHost(MESSAGEBOX_HOST)
|
|
117
|
+
const result2 = await clientA.anointHost(MESSAGEBOX_HOST)
|
|
118
|
+
expect(result1.txid).not.toBe(result2.txid)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('clientA sends and acknowledges multiple messages to clientB', async () => {
|
|
122
|
+
const contents = ['msg1', 'msg2', 'msg3']
|
|
123
|
+
for (const msg of contents) {
|
|
124
|
+
const response = await clientA.sendMessage({
|
|
125
|
+
recipient: identityKeyB,
|
|
126
|
+
messageBox: peerBox,
|
|
127
|
+
body: msg
|
|
128
|
+
})
|
|
129
|
+
expect(response.status).toBe('success')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
133
|
+
const messages = await clientB.listMessages({ messageBox: peerBox })
|
|
134
|
+
const matched = contents.every(c => messages.some(m => m.body.includes(c)))
|
|
135
|
+
expect(matched).toBe(true)
|
|
136
|
+
|
|
137
|
+
const ids = messages.map(m => m.messageId).filter(Boolean)
|
|
138
|
+
const status = await clientB.acknowledgeMessage({ messageIds: ids })
|
|
139
|
+
expect(status).toBe('success')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('clientA reinitializes with init() and correctly anoints host', async () => {
|
|
143
|
+
const tempClient = new MessageBoxClient({
|
|
144
|
+
walletClient: walletA,
|
|
145
|
+
networkPreset: 'local',
|
|
146
|
+
enableLogging: true
|
|
147
|
+
// No host provided here!
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Call init manually
|
|
151
|
+
await tempClient.init(MESSAGEBOX_HOST)
|
|
152
|
+
|
|
153
|
+
// Verify client is initialized and host is correct
|
|
154
|
+
expect((tempClient as any).initialized).toBe(true)
|
|
155
|
+
expect((tempClient as any).host).toBe(MESSAGEBOX_HOST)
|
|
156
|
+
|
|
157
|
+
// Optionally, test that resolving our own identity also works
|
|
158
|
+
const resolvedHost = await (tempClient as any).resolveHostForRecipient(identityKeyA)
|
|
159
|
+
expect(resolvedHost).toBe(MESSAGEBOX_HOST)
|
|
160
|
+
|
|
161
|
+
await tempClient.disconnectWebSocket()
|
|
162
|
+
})
|
|
163
|
+
})
|