@bopen-io/messagebox-server 1.1.5 → 1.2.0
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/.dockerignore +11 -0
- package/.env.example +11 -0
- package/.eslintignore +2 -0
- package/.gcloudignore +17 -0
- package/.gitattributes +2 -0
- package/.github/workflows/build-and-push.yml +51 -0
- package/.github/workflows/deploy.yaml.example +64 -0
- package/.github/workflows/image.yaml +66 -0
- package/.vscode/launch.json +67 -0
- package/AGENTS.md +92 -0
- package/BASELINE.md +55 -0
- package/DEPLOYING.md +120 -0
- package/Dockerfile +63 -0
- package/docker-compose.yml +64 -0
- package/jest.config.integration.ts +26 -0
- package/jest.config.mjs +42 -0
- package/jest.config.ts +35 -0
- package/jest.setup.mjs +18 -0
- package/nginx.conf +36 -0
- package/package.json +65 -51
- package/scripts/build.js +19 -0
- package/scripts/mkenv.sh +31 -0
- package/scripts/start.sh +16 -0
- package/src/__tests/compose.test.ts +30 -0
- package/src/app.ts +210 -0
- package/src/compose.ts +286 -0
- package/src/config/firebase.ts +183 -0
- package/src/context.ts +49 -0
- package/src/index.ts +375 -0
- package/src/routes/__tests/acknowledgeMessage.test.ts +158 -0
- package/src/routes/__tests/listMessages.test.ts +240 -0
- package/src/routes/__tests/sendMessage.test.ts +362 -0
- package/src/routes/acknowledgeMessage.ts +153 -0
- package/src/routes/index.ts +17 -0
- package/src/routes/listDevices.ts +80 -0
- package/src/routes/listMessages.ts +195 -0
- package/src/routes/permissions/getPermission.ts +152 -0
- package/src/routes/permissions/getQuote.ts +192 -0
- package/src/routes/permissions/index.ts +12 -0
- package/src/routes/permissions/listPermissions.ts +202 -0
- package/src/routes/permissions/setPermission.ts +155 -0
- package/src/routes/registerDevice.ts +155 -0
- package/src/routes/sendMessage.ts +531 -0
- package/src/runtimeDeps.ts +29 -0
- package/src/swagger.ts +54 -0
- package/src/telemetry.ts +158 -0
- package/src/types/messagePermissions.ts +18 -0
- package/src/types/mock-knex.d.ts +18 -0
- package/src/types/notifications.ts +27 -0
- package/src/utils/logger.ts +92 -0
- package/src/utils/messagePermissions.ts +148 -0
- package/src/utils/sendFCMNotification.ts +143 -0
- package/tsconfig.json +36 -0
- package/tsconfig.types.json +11 -0
- package/out/knexfile.js +0 -25
- package/out/knexfile.js.map +0 -1
- package/out/src/app.js +0 -160
- package/out/src/app.js.map +0 -1
- package/out/src/config/firebase.js +0 -150
- package/out/src/config/firebase.js.map +0 -1
- package/out/src/index.js +0 -295
- package/out/src/index.js.map +0 -1
- package/out/src/migrations/2022-12-28-001-initial-migration.js +0 -28
- package/out/src/migrations/2022-12-28-001-initial-migration.js.map +0 -1
- package/out/src/migrations/2023-01-17-messages-update.js +0 -11
- package/out/src/migrations/2023-01-17-messages-update.js.map +0 -1
- package/out/src/migrations/2024-03-05-001-messageID-upgrade.js +0 -21
- package/out/src/migrations/2024-03-05-001-messageID-upgrade.js.map +0 -1
- package/out/src/migrations/2025-01-31-001-notification-permissions.js +0 -59
- package/out/src/migrations/2025-01-31-001-notification-permissions.js.map +0 -1
- package/out/src/migrations/2025-01-31-002-device-registrations.js +0 -24
- package/out/src/migrations/2025-01-31-002-device-registrations.js.map +0 -1
- package/out/src/routes/__tests/acknowledgeMessage.test.js +0 -129
- package/out/src/routes/__tests/acknowledgeMessage.test.js.map +0 -1
- package/out/src/routes/__tests/listMessages.test.js +0 -211
- package/out/src/routes/__tests/listMessages.test.js.map +0 -1
- package/out/src/routes/__tests/sendMessage.test.js +0 -307
- package/out/src/routes/__tests/sendMessage.test.js.map +0 -1
- package/out/src/routes/acknowledgeMessage.js +0 -144
- package/out/src/routes/acknowledgeMessage.js.map +0 -1
- package/out/src/routes/index.js +0 -17
- package/out/src/routes/index.js.map +0 -1
- package/out/src/routes/listDevices.js +0 -74
- package/out/src/routes/listDevices.js.map +0 -1
- package/out/src/routes/listMessages.js +0 -186
- package/out/src/routes/listMessages.js.map +0 -1
- package/out/src/routes/permissions/getPermission.js +0 -149
- package/out/src/routes/permissions/getPermission.js.map +0 -1
- package/out/src/routes/permissions/getQuote.js +0 -165
- package/out/src/routes/permissions/getQuote.js.map +0 -1
- package/out/src/routes/permissions/index.js +0 -12
- package/out/src/routes/permissions/index.js.map +0 -1
- package/out/src/routes/permissions/listPermissions.js +0 -190
- package/out/src/routes/permissions/listPermissions.js.map +0 -1
- package/out/src/routes/permissions/setPermission.js +0 -137
- package/out/src/routes/permissions/setPermission.js.map +0 -1
- package/out/src/routes/registerDevice.js +0 -150
- package/out/src/routes/registerDevice.js.map +0 -1
- package/out/src/routes/sendMessage.js +0 -451
- package/out/src/routes/sendMessage.js.map +0 -1
- package/out/src/swagger.js +0 -49
- package/out/src/swagger.js.map +0 -1
- package/out/src/types/messagePermissions.js +0 -2
- package/out/src/types/messagePermissions.js.map +0 -1
- package/out/src/types/notifications.js +0 -2
- package/out/src/types/notifications.js.map +0 -1
- package/out/src/utils/logger.js +0 -24
- package/out/src/utils/logger.js.map +0 -1
- package/out/src/utils/messagePermissions.js +0 -126
- package/out/src/utils/messagePermissions.js.map +0 -1
- package/out/src/utils/sendFCMNotification.js +0 -121
- package/out/src/utils/sendFCMNotification.js.map +0 -1
- package/out/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
import listMessages from '../listMessages.js'
|
|
3
|
+
import mockKnex, { Tracker } from 'mock-knex'
|
|
4
|
+
import { Response } from 'express'
|
|
5
|
+
import { AuthRequest } from '@bsv/auth-express-middleware'
|
|
6
|
+
import knexLib from 'knex'
|
|
7
|
+
import knexConfig from '../../../knexfile.js'
|
|
8
|
+
import { bindMessageBoxRuntime } from '../../runtimeDeps.js'
|
|
9
|
+
|
|
10
|
+
// Ensure proper handling of mock-knex
|
|
11
|
+
const testKnex = (knexLib as any).default?.(knexConfig.development) ??
|
|
12
|
+
(knexLib as any)(knexConfig.development)
|
|
13
|
+
bindMessageBoxRuntime({ knex: testKnex })
|
|
14
|
+
const knex = listMessages.knex
|
|
15
|
+
let queryTracker: Tracker
|
|
16
|
+
|
|
17
|
+
// Define Mock Express Response Object
|
|
18
|
+
const mockRes: jest.Mocked<Response> = {
|
|
19
|
+
status: jest.fn().mockReturnThis(),
|
|
20
|
+
json: jest.fn().mockReturnThis(),
|
|
21
|
+
sendStatus: jest.fn().mockReturnThis(),
|
|
22
|
+
send: jest.fn().mockReturnThis(),
|
|
23
|
+
end: jest.fn().mockReturnThis(),
|
|
24
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
25
|
+
getHeader: jest.fn(),
|
|
26
|
+
getHeaders: jest.fn(),
|
|
27
|
+
header: jest.fn().mockReturnThis(),
|
|
28
|
+
type: jest.fn().mockReturnThis(),
|
|
29
|
+
format: jest.fn(),
|
|
30
|
+
location: jest.fn().mockReturnThis(),
|
|
31
|
+
redirect: jest.fn().mockReturnThis(),
|
|
32
|
+
append: jest.fn().mockReturnThis(),
|
|
33
|
+
render: jest.fn(),
|
|
34
|
+
vary: jest.fn().mockReturnThis(),
|
|
35
|
+
cookie: jest.fn().mockReturnThis(),
|
|
36
|
+
clearCookie: jest.fn().mockReturnThis()
|
|
37
|
+
} as unknown as jest.Mocked<Response>
|
|
38
|
+
|
|
39
|
+
let validReq: AuthRequest
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
41
|
+
let validRes: { status: string, messages: any[] }
|
|
42
|
+
let validMessageBoxes: Array<{ messageBoxId: number }>
|
|
43
|
+
let validMessages: Array<{ sender: string, messageId: string, body: string, created_at: string, updated_at: string }>
|
|
44
|
+
let expectedMessages: Array<{ sender: string, messageId: string, body: string, createdAt: string, updatedAt: string }>
|
|
45
|
+
|
|
46
|
+
describe('listMessages', () => {
|
|
47
|
+
beforeAll(() => {
|
|
48
|
+
(mockKnex as any).mock(knex)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
jest.spyOn(console, 'error').mockImplementation(() => { })
|
|
53
|
+
|
|
54
|
+
queryTracker = (mockKnex as any).getTracker() as Tracker
|
|
55
|
+
queryTracker.install()
|
|
56
|
+
|
|
57
|
+
validMessages = [{
|
|
58
|
+
sender: '028d37b941208cd6b8a4c28288eda5f2f16c2b3ab0fcb6d13c18b47fe37b971fc1',
|
|
59
|
+
messageId: 'msg-1',
|
|
60
|
+
body: '{}',
|
|
61
|
+
created_at: '2024-01-01',
|
|
62
|
+
updated_at: '2024-01-01'
|
|
63
|
+
}]
|
|
64
|
+
expectedMessages = [{
|
|
65
|
+
sender: '028d37b941208cd6b8a4c28288eda5f2f16c2b3ab0fcb6d13c18b47fe37b971fc1',
|
|
66
|
+
messageId: 'msg-1',
|
|
67
|
+
body: '{}',
|
|
68
|
+
createdAt: '2024-01-01',
|
|
69
|
+
updatedAt: '2024-01-01'
|
|
70
|
+
}]
|
|
71
|
+
|
|
72
|
+
// Mock Data
|
|
73
|
+
validRes = {
|
|
74
|
+
status: 'success',
|
|
75
|
+
messages: validMessages
|
|
76
|
+
}
|
|
77
|
+
validMessageBoxes = [
|
|
78
|
+
{ messageBoxId: 42 },
|
|
79
|
+
{ messageBoxId: 31 }
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
// Fully typed mock request
|
|
83
|
+
validReq = {
|
|
84
|
+
auth: {
|
|
85
|
+
identityKey: 'mockIdKey'
|
|
86
|
+
},
|
|
87
|
+
body: {
|
|
88
|
+
messageBox: 'payment_inbox'
|
|
89
|
+
},
|
|
90
|
+
get: jest.fn(),
|
|
91
|
+
header: jest.fn()
|
|
92
|
+
} as unknown as AuthRequest
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
jest.clearAllMocks()
|
|
97
|
+
|
|
98
|
+
if (queryTracker !== null && queryTracker !== undefined) {
|
|
99
|
+
queryTracker.uninstall()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
afterAll(() => {
|
|
104
|
+
(mockKnex as any).unmock(knex)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('Throws an error if a messageBox is not provided', async () => {
|
|
108
|
+
validReq.body.messageBox = undefined
|
|
109
|
+
queryTracker.on('query', (q) => {
|
|
110
|
+
q.response([])
|
|
111
|
+
})
|
|
112
|
+
await listMessages.func(validReq, mockRes as Response)
|
|
113
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
114
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
115
|
+
status: 'error',
|
|
116
|
+
code: 'ERR_MESSAGEBOX_REQUIRED',
|
|
117
|
+
description: 'Please provide the name of a valid MessageBox!'
|
|
118
|
+
}))
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('Throws an error if messageBox is not a string', async () => {
|
|
122
|
+
validReq.body.messageBox = 123 as unknown as string
|
|
123
|
+
queryTracker.on('query', (q) => {
|
|
124
|
+
q.response([])
|
|
125
|
+
})
|
|
126
|
+
await listMessages.func(validReq, mockRes as Response)
|
|
127
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
128
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
129
|
+
status: 'error',
|
|
130
|
+
code: 'ERR_INVALID_MESSAGEBOX',
|
|
131
|
+
description: 'MessageBox name must be a string!'
|
|
132
|
+
}))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('Throws an error if no matching messageBox is found', async () => {
|
|
136
|
+
validReq.body.messageBox = 'pay_inbox'
|
|
137
|
+
queryTracker.on('query', (q, s) => {
|
|
138
|
+
if (s === 1) {
|
|
139
|
+
expect(q.method).toEqual('select')
|
|
140
|
+
expect(q.sql).toEqual(
|
|
141
|
+
'select `messageBoxId` from `messageBox` where `identityKey` = ? and `type` = ?'
|
|
142
|
+
)
|
|
143
|
+
q.response([undefined])
|
|
144
|
+
} else {
|
|
145
|
+
q.response([])
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
await listMessages.func(validReq, mockRes as Response)
|
|
149
|
+
expect(mockRes.status).toHaveBeenCalledWith(200)
|
|
150
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
151
|
+
status: 'success',
|
|
152
|
+
messages: []
|
|
153
|
+
}))
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('Returns ID of messageBox', async () => {
|
|
157
|
+
queryTracker.on('query', (q, s) => {
|
|
158
|
+
if (s === 1) {
|
|
159
|
+
expect(q.method).toEqual('select')
|
|
160
|
+
expect(q.sql).toEqual(
|
|
161
|
+
'select `messageBoxId` from `messageBox` where `identityKey` = ? and `type` = ?'
|
|
162
|
+
)
|
|
163
|
+
expect(q.bindings).toEqual([
|
|
164
|
+
'mockIdKey',
|
|
165
|
+
'payment_inbox'
|
|
166
|
+
])
|
|
167
|
+
q.response([validMessageBoxes[0]])
|
|
168
|
+
} else if (s === 2) {
|
|
169
|
+
q.response(validMessages)
|
|
170
|
+
} else {
|
|
171
|
+
q.response([])
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
await listMessages.func(validReq, mockRes as Response)
|
|
175
|
+
expect(mockRes.status).toHaveBeenCalledWith(200)
|
|
176
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
177
|
+
status: 'success',
|
|
178
|
+
messages: expectedMessages
|
|
179
|
+
}))
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('Returns empty array if no messages found', async () => {
|
|
183
|
+
queryTracker.on('query', (q, s) => {
|
|
184
|
+
if (s === 1) {
|
|
185
|
+
q.response([{ messageBoxId: 123 }])
|
|
186
|
+
} else if (s === 2) {
|
|
187
|
+
expect(q.method).toEqual('select')
|
|
188
|
+
expect(q.sql).toEqual(
|
|
189
|
+
'select `messageId`, `body`, `sender`, `created_at`, `updated_at` from `messages` where `recipient` = ? and `messageBoxId` = ?'
|
|
190
|
+
)
|
|
191
|
+
q.response([])
|
|
192
|
+
} else {
|
|
193
|
+
q.response([])
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
await listMessages.func(validReq, mockRes as Response)
|
|
197
|
+
expect(mockRes.status).toHaveBeenCalledWith(200)
|
|
198
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
199
|
+
status: 'success',
|
|
200
|
+
messages: []
|
|
201
|
+
}))
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('Returns list of messages found', async () => {
|
|
205
|
+
queryTracker.on('query', (q, s) => {
|
|
206
|
+
if (s === 1) {
|
|
207
|
+
q.response([{ messageBoxId: 123 }])
|
|
208
|
+
} else if (s === 2) {
|
|
209
|
+
expect(q.method).toEqual('select')
|
|
210
|
+
expect(q.sql).toEqual(
|
|
211
|
+
'select `messageId`, `body`, `sender`, `created_at`, `updated_at` from `messages` where `recipient` = ? and `messageBoxId` = ?'
|
|
212
|
+
)
|
|
213
|
+
q.response(validMessages)
|
|
214
|
+
} else {
|
|
215
|
+
q.response([])
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
await listMessages.func(validReq, mockRes as Response)
|
|
219
|
+
expect(mockRes.status).toHaveBeenCalledWith(200)
|
|
220
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
221
|
+
status: 'success',
|
|
222
|
+
messages: expectedMessages
|
|
223
|
+
}))
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('Throws unknown errors', async () => {
|
|
227
|
+
queryTracker.on('query', () => {
|
|
228
|
+
throw new Error('Failed')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
await listMessages.func(validReq, mockRes as Response)
|
|
232
|
+
|
|
233
|
+
expect(mockRes.status).toHaveBeenCalledWith(500)
|
|
234
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
235
|
+
status: 'error',
|
|
236
|
+
code: 'ERR_INTERNAL_ERROR',
|
|
237
|
+
description: 'An internal error has occurred while listing messages.'
|
|
238
|
+
}))
|
|
239
|
+
})
|
|
240
|
+
})
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
import sendMessage, { calculateMessagePrice, Message, SendMessageRequest } from '../sendMessage.js'
|
|
3
|
+
import mockKnex from 'mock-knex'
|
|
4
|
+
import { Response } from 'express'
|
|
5
|
+
import type { Tracker } from 'mock-knex'
|
|
6
|
+
import knexLib from 'knex'
|
|
7
|
+
import knexConfig from '../../../knexfile.js'
|
|
8
|
+
import { bindMessageBoxRuntime } from '../../runtimeDeps.js'
|
|
9
|
+
import { Logger } from '../../utils/logger.js'
|
|
10
|
+
import axios from 'axios'
|
|
11
|
+
import type { AxiosInstance as AxiosInstanceType } from 'axios'
|
|
12
|
+
import AxiosMockAdapter from 'axios-mock-adapter'
|
|
13
|
+
|
|
14
|
+
global.fetch = jest.fn()
|
|
15
|
+
|
|
16
|
+
const testKnex = (knexLib as any).default?.(knexConfig.development) ??
|
|
17
|
+
(knexLib as any)(knexConfig.development)
|
|
18
|
+
bindMessageBoxRuntime({ knex: testKnex })
|
|
19
|
+
const knex = sendMessage.knex
|
|
20
|
+
let queryTracker: Tracker
|
|
21
|
+
let axiosMock: AxiosMockAdapter
|
|
22
|
+
|
|
23
|
+
// Define Mock Express Response Object
|
|
24
|
+
const mockRes: jest.Mocked<Response> = {
|
|
25
|
+
status: jest.fn().mockReturnThis(),
|
|
26
|
+
json: jest.fn().mockReturnThis(),
|
|
27
|
+
sendStatus: jest.fn().mockReturnThis(),
|
|
28
|
+
send: jest.fn().mockReturnThis(),
|
|
29
|
+
end: jest.fn().mockReturnThis(),
|
|
30
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
31
|
+
getHeader: jest.fn(),
|
|
32
|
+
getHeaders: jest.fn(),
|
|
33
|
+
header: jest.fn().mockReturnThis(),
|
|
34
|
+
type: jest.fn().mockReturnThis(),
|
|
35
|
+
format: jest.fn(),
|
|
36
|
+
location: jest.fn().mockReturnThis(),
|
|
37
|
+
redirect: jest.fn().mockReturnThis(),
|
|
38
|
+
append: jest.fn().mockReturnThis(),
|
|
39
|
+
render: jest.fn(),
|
|
40
|
+
vary: jest.fn().mockReturnThis(),
|
|
41
|
+
cookie: jest.fn().mockReturnThis(),
|
|
42
|
+
clearCookie: jest.fn().mockReturnThis()
|
|
43
|
+
} as unknown as jest.Mocked<Response>
|
|
44
|
+
|
|
45
|
+
let validReq: SendMessageRequest
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
47
|
+
let validRes: { status: string }
|
|
48
|
+
let validMessageBox: { messageBoxId: number, type: string }
|
|
49
|
+
|
|
50
|
+
describe('sendMessage', () => {
|
|
51
|
+
// Capture original console methods
|
|
52
|
+
const originalError = console.error
|
|
53
|
+
const originalLog = console.log
|
|
54
|
+
const originalWarn = console.warn
|
|
55
|
+
|
|
56
|
+
beforeAll(() => {
|
|
57
|
+
mockKnex.mock(knex)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
Logger.enable()
|
|
62
|
+
|
|
63
|
+
jest.spyOn(console, 'error').mockImplementation((...args) => originalError(...args))
|
|
64
|
+
jest.spyOn(console, 'log').mockImplementation((...args) => originalLog(...args))
|
|
65
|
+
jest.spyOn(console, 'warn').mockImplementation((...args) => originalWarn(...args))
|
|
66
|
+
|
|
67
|
+
const instance: AxiosInstanceType = axios
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
axiosMock = new AxiosMockAdapter(instance)
|
|
71
|
+
|
|
72
|
+
queryTracker = mockKnex.getTracker()
|
|
73
|
+
queryTracker.install()
|
|
74
|
+
|
|
75
|
+
// Mock Data
|
|
76
|
+
validRes = {
|
|
77
|
+
status: 'success'
|
|
78
|
+
}
|
|
79
|
+
validMessageBox = {
|
|
80
|
+
messageBoxId: 42,
|
|
81
|
+
type: 'payment_inbox'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
validReq = {
|
|
85
|
+
auth: {
|
|
86
|
+
identityKey: 'mockIdKey'
|
|
87
|
+
},
|
|
88
|
+
body: {
|
|
89
|
+
message: {
|
|
90
|
+
messageId: 'mock-message-id',
|
|
91
|
+
recipient: '028d37b941208cd6b8a4c28288eda5f2f16c2b3ab0fcb6d13c18b47fe37b971fc1',
|
|
92
|
+
messageBox: 'payment_inbox',
|
|
93
|
+
body: JSON.stringify({})
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
get: jest.fn(),
|
|
97
|
+
header: jest.fn()
|
|
98
|
+
} as unknown as SendMessageRequest
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
jest.clearAllMocks()
|
|
103
|
+
|
|
104
|
+
if (queryTracker !== null && queryTracker !== undefined) {
|
|
105
|
+
queryTracker.uninstall()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
axiosMock?.restore()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
afterAll(() => {
|
|
112
|
+
mockKnex.unmock(knex)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('Throws an error if message is missing', async () => {
|
|
116
|
+
validReq.body = {} // Ensure body exists, but message is missing
|
|
117
|
+
|
|
118
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
119
|
+
|
|
120
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
121
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
122
|
+
status: 'error',
|
|
123
|
+
code: 'ERR_MESSAGE_REQUIRED',
|
|
124
|
+
description: 'Please provide a valid message to send!'
|
|
125
|
+
}))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('Throws an error if message is not an object', async () => {
|
|
129
|
+
validReq.body.message = 'My message to send' as unknown as Message
|
|
130
|
+
|
|
131
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
132
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
133
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
134
|
+
status: 'error',
|
|
135
|
+
code: 'ERR_INVALID_MESSAGEBOX',
|
|
136
|
+
description: 'Invalid message box.'
|
|
137
|
+
}))
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('Throws an error if recipient is missing', async () => {
|
|
141
|
+
if (validReq.body.message !== null && validReq.body.message !== undefined) {
|
|
142
|
+
validReq.body.message.recipient = undefined as unknown as string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
146
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
147
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
148
|
+
status: 'error',
|
|
149
|
+
code: 'ERR_RECIPIENT_REQUIRED',
|
|
150
|
+
description: 'Missing recipient(s). Provide "recipient" or "recipients".'
|
|
151
|
+
}))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('Throws an error if recipient is not a string', async () => {
|
|
155
|
+
if (validReq.body.message !== null && validReq.body.message !== undefined) {
|
|
156
|
+
validReq.body.message.recipient = 123 as unknown as string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
160
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
161
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
162
|
+
status: 'error',
|
|
163
|
+
code: 'ERR_INVALID_RECIPIENT_KEY'
|
|
164
|
+
}))
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('Returns error if messageBox is missing', async () => {
|
|
168
|
+
if (validReq.body.message !== null && validReq.body.message !== undefined) {
|
|
169
|
+
validReq.body.message.messageBox = undefined as unknown as string
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
173
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
174
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
175
|
+
status: 'error',
|
|
176
|
+
code: 'ERR_INVALID_MESSAGEBOX'
|
|
177
|
+
}))
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('Throws an error if messageBox is not a string', async () => {
|
|
181
|
+
if (validReq.body.message !== null && validReq.body.message !== undefined) {
|
|
182
|
+
validReq.body.message.messageBox = 123 as unknown as string
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
186
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
187
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
188
|
+
status: 'error',
|
|
189
|
+
code: 'ERR_INVALID_MESSAGEBOX',
|
|
190
|
+
description: 'Invalid message box.'
|
|
191
|
+
}))
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('Throws an error if the message body is not a string', async () => {
|
|
195
|
+
if (validReq.body.message !== null && validReq.body.message !== undefined) {
|
|
196
|
+
validReq.body.message.body = 42 as unknown as string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
200
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
201
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
202
|
+
status: 'error',
|
|
203
|
+
code: 'ERR_INVALID_MESSAGE_BODY',
|
|
204
|
+
description: 'Invalid message body.'
|
|
205
|
+
}))
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('Returns error if message body is missing', async () => {
|
|
209
|
+
if (validReq.body.message !== null && validReq.body.message !== undefined) {
|
|
210
|
+
validReq.body.message.body = undefined as unknown as string
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
214
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
215
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
216
|
+
status: 'error',
|
|
217
|
+
code: 'ERR_INVALID_MESSAGE_BODY',
|
|
218
|
+
description: 'Invalid message body.'
|
|
219
|
+
}))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('Returns base price for an empty message', () => {
|
|
223
|
+
const price = calculateMessagePrice('')
|
|
224
|
+
expect(price).toBe(2) // Base price only
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('Calculates price for a small message (below 1KB)', () => {
|
|
228
|
+
const price = calculateMessagePrice('Hello, world!')
|
|
229
|
+
expect(price).toBe(5) // Base price only
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('Calculates price for a 2KB message', () => {
|
|
233
|
+
const message = 'a'.repeat(2048) // 2KB
|
|
234
|
+
const price = calculateMessagePrice(message)
|
|
235
|
+
expect(price).toBe(8) // Base price + 2KB
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('Adds priority fee when enabled', () => {
|
|
239
|
+
const price = calculateMessagePrice('Hello', true)
|
|
240
|
+
expect(price).toBe(5) // Base price
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('Handles large messages (5KB)', () => {
|
|
244
|
+
const message = 'a'.repeat(5120) // 5KB
|
|
245
|
+
const price = calculateMessagePrice(message)
|
|
246
|
+
expect(price).toBe(17) // Base price + 5KB
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('Handles edge case of exactly 1KB message', () => {
|
|
250
|
+
const message = 'a'.repeat(1024) // Exactly 1KB
|
|
251
|
+
const price = calculateMessagePrice(message)
|
|
252
|
+
expect(price).toBe(5) // Base price + 1KB
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('Handles edge case of 1KB + 1 byte message', () => {
|
|
256
|
+
const message = 'a'.repeat(1025) // 1KB + 1 byte
|
|
257
|
+
const price = calculateMessagePrice(message)
|
|
258
|
+
expect(price).toBe(8) // Rounded up to 2KB
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('Handles messages larger than 10KB', () => {
|
|
262
|
+
const message = 'a'.repeat(10240) // 10KB
|
|
263
|
+
const price = calculateMessagePrice(message)
|
|
264
|
+
expect(price).toBe(32) // Base price + 10KB
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('Returns error if messageId is missing', async () => {
|
|
268
|
+
if (validReq.body.message !== undefined && validReq.body.message !== null) {
|
|
269
|
+
validReq.body.message.messageId = undefined as unknown as string
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
273
|
+
|
|
274
|
+
expect(mockRes.status).toHaveBeenCalledWith(400)
|
|
275
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
276
|
+
status: 'error',
|
|
277
|
+
code: 'ERR_MESSAGEID_REQUIRED',
|
|
278
|
+
description: 'Missing messageId.'
|
|
279
|
+
}))
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('Creates a messageBox when it does not exist', async () => {
|
|
283
|
+
queryTracker.on('query', (q, step: number) => {
|
|
284
|
+
if (step === 1) {
|
|
285
|
+
q.response(0) // Simulate that the messageBox does not exist
|
|
286
|
+
} else if (step === 2) {
|
|
287
|
+
q.response([validMessageBox]) // Simulate messageBox being inserted
|
|
288
|
+
} else if (step === 3) {
|
|
289
|
+
q.response([validMessageBox]) // Simulate finding a valid messageBoxId
|
|
290
|
+
} else {
|
|
291
|
+
q.response([]) // Default response
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
296
|
+
|
|
297
|
+
expect(mockRes.status).toHaveBeenCalledWith(200)
|
|
298
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
299
|
+
status: 'success'
|
|
300
|
+
}))
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('Silently ignores duplicate messages via onConflict().ignore()', async () => {
|
|
304
|
+
queryTracker.on('query', (q, step: number) => {
|
|
305
|
+
if (step === 1) {
|
|
306
|
+
q.response({ messageBoxId: 42, type: 'payment_inbox' }) // messageBox exists
|
|
307
|
+
} else if (step === 2) {
|
|
308
|
+
q.response({ messageBoxId: 42 }) // get messageBoxId for insert
|
|
309
|
+
} else if (step === 3) {
|
|
310
|
+
q.response(0) // insert with onConflict().ignore() returns 0 rows affected
|
|
311
|
+
} else {
|
|
312
|
+
q.response([])
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
317
|
+
|
|
318
|
+
expect(mockRes.status).toHaveBeenCalledWith(200)
|
|
319
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
320
|
+
status: 'success'
|
|
321
|
+
}))
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('Returns internal error if unexpected error occurs', async () => {
|
|
325
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
326
|
+
jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
327
|
+
queryTracker.on('query', () => {
|
|
328
|
+
throw new Error('Unexpected failure') // Simulating an unexpected database failure
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
await sendMessage.func(validReq, mockRes as Response)
|
|
332
|
+
|
|
333
|
+
// Ensure the response status is set
|
|
334
|
+
expect(mockRes.status).toHaveBeenCalledTimes(1)
|
|
335
|
+
expect(mockRes.status).toHaveBeenCalledWith(500)
|
|
336
|
+
|
|
337
|
+
// Ensure the response body is set
|
|
338
|
+
expect(mockRes.json).toHaveBeenCalledTimes(1)
|
|
339
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
340
|
+
status: 'error',
|
|
341
|
+
code: 'ERR_INTERNAL'
|
|
342
|
+
}))
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
it('creates a new messageBox when one does not exist for recipient', async () => {
|
|
347
|
+
queryTracker.on('query', (q, step) => {
|
|
348
|
+
if (step === 1) q.response(undefined) // messageBox not found
|
|
349
|
+
else if (step === 2) q.response(1) // messageBox insert
|
|
350
|
+
else if (step === 3) q.response({ messageBoxId: 42 }) // get messageBoxId for message insert
|
|
351
|
+
else if (step === 4) q.response(1) // insert message
|
|
352
|
+
else q.response([])
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
await sendMessage.func(validReq, mockRes)
|
|
356
|
+
|
|
357
|
+
expect(mockRes.status).toHaveBeenCalledWith(200)
|
|
358
|
+
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
359
|
+
status: 'success'
|
|
360
|
+
}))
|
|
361
|
+
})
|
|
362
|
+
})
|