@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.
Files changed (113) hide show
  1. package/.dockerignore +11 -0
  2. package/.env.example +11 -0
  3. package/.eslintignore +2 -0
  4. package/.gcloudignore +17 -0
  5. package/.gitattributes +2 -0
  6. package/.github/workflows/build-and-push.yml +51 -0
  7. package/.github/workflows/deploy.yaml.example +64 -0
  8. package/.github/workflows/image.yaml +66 -0
  9. package/.vscode/launch.json +67 -0
  10. package/AGENTS.md +92 -0
  11. package/BASELINE.md +55 -0
  12. package/DEPLOYING.md +120 -0
  13. package/Dockerfile +63 -0
  14. package/docker-compose.yml +64 -0
  15. package/jest.config.integration.ts +26 -0
  16. package/jest.config.mjs +42 -0
  17. package/jest.config.ts +35 -0
  18. package/jest.setup.mjs +18 -0
  19. package/nginx.conf +36 -0
  20. package/package.json +65 -51
  21. package/scripts/build.js +19 -0
  22. package/scripts/mkenv.sh +31 -0
  23. package/scripts/start.sh +16 -0
  24. package/src/__tests/compose.test.ts +30 -0
  25. package/src/app.ts +210 -0
  26. package/src/compose.ts +286 -0
  27. package/src/config/firebase.ts +183 -0
  28. package/src/context.ts +49 -0
  29. package/src/index.ts +375 -0
  30. package/src/routes/__tests/acknowledgeMessage.test.ts +158 -0
  31. package/src/routes/__tests/listMessages.test.ts +240 -0
  32. package/src/routes/__tests/sendMessage.test.ts +362 -0
  33. package/src/routes/acknowledgeMessage.ts +153 -0
  34. package/src/routes/index.ts +17 -0
  35. package/src/routes/listDevices.ts +80 -0
  36. package/src/routes/listMessages.ts +195 -0
  37. package/src/routes/permissions/getPermission.ts +152 -0
  38. package/src/routes/permissions/getQuote.ts +192 -0
  39. package/src/routes/permissions/index.ts +12 -0
  40. package/src/routes/permissions/listPermissions.ts +202 -0
  41. package/src/routes/permissions/setPermission.ts +155 -0
  42. package/src/routes/registerDevice.ts +155 -0
  43. package/src/routes/sendMessage.ts +531 -0
  44. package/src/runtimeDeps.ts +29 -0
  45. package/src/swagger.ts +54 -0
  46. package/src/telemetry.ts +158 -0
  47. package/src/types/messagePermissions.ts +18 -0
  48. package/src/types/mock-knex.d.ts +18 -0
  49. package/src/types/notifications.ts +27 -0
  50. package/src/utils/logger.ts +92 -0
  51. package/src/utils/messagePermissions.ts +148 -0
  52. package/src/utils/sendFCMNotification.ts +143 -0
  53. package/tsconfig.json +36 -0
  54. package/tsconfig.types.json +11 -0
  55. package/out/knexfile.js +0 -25
  56. package/out/knexfile.js.map +0 -1
  57. package/out/src/app.js +0 -160
  58. package/out/src/app.js.map +0 -1
  59. package/out/src/config/firebase.js +0 -150
  60. package/out/src/config/firebase.js.map +0 -1
  61. package/out/src/index.js +0 -295
  62. package/out/src/index.js.map +0 -1
  63. package/out/src/migrations/2022-12-28-001-initial-migration.js +0 -28
  64. package/out/src/migrations/2022-12-28-001-initial-migration.js.map +0 -1
  65. package/out/src/migrations/2023-01-17-messages-update.js +0 -11
  66. package/out/src/migrations/2023-01-17-messages-update.js.map +0 -1
  67. package/out/src/migrations/2024-03-05-001-messageID-upgrade.js +0 -21
  68. package/out/src/migrations/2024-03-05-001-messageID-upgrade.js.map +0 -1
  69. package/out/src/migrations/2025-01-31-001-notification-permissions.js +0 -59
  70. package/out/src/migrations/2025-01-31-001-notification-permissions.js.map +0 -1
  71. package/out/src/migrations/2025-01-31-002-device-registrations.js +0 -24
  72. package/out/src/migrations/2025-01-31-002-device-registrations.js.map +0 -1
  73. package/out/src/routes/__tests/acknowledgeMessage.test.js +0 -129
  74. package/out/src/routes/__tests/acknowledgeMessage.test.js.map +0 -1
  75. package/out/src/routes/__tests/listMessages.test.js +0 -211
  76. package/out/src/routes/__tests/listMessages.test.js.map +0 -1
  77. package/out/src/routes/__tests/sendMessage.test.js +0 -307
  78. package/out/src/routes/__tests/sendMessage.test.js.map +0 -1
  79. package/out/src/routes/acknowledgeMessage.js +0 -144
  80. package/out/src/routes/acknowledgeMessage.js.map +0 -1
  81. package/out/src/routes/index.js +0 -17
  82. package/out/src/routes/index.js.map +0 -1
  83. package/out/src/routes/listDevices.js +0 -74
  84. package/out/src/routes/listDevices.js.map +0 -1
  85. package/out/src/routes/listMessages.js +0 -186
  86. package/out/src/routes/listMessages.js.map +0 -1
  87. package/out/src/routes/permissions/getPermission.js +0 -149
  88. package/out/src/routes/permissions/getPermission.js.map +0 -1
  89. package/out/src/routes/permissions/getQuote.js +0 -165
  90. package/out/src/routes/permissions/getQuote.js.map +0 -1
  91. package/out/src/routes/permissions/index.js +0 -12
  92. package/out/src/routes/permissions/index.js.map +0 -1
  93. package/out/src/routes/permissions/listPermissions.js +0 -190
  94. package/out/src/routes/permissions/listPermissions.js.map +0 -1
  95. package/out/src/routes/permissions/setPermission.js +0 -137
  96. package/out/src/routes/permissions/setPermission.js.map +0 -1
  97. package/out/src/routes/registerDevice.js +0 -150
  98. package/out/src/routes/registerDevice.js.map +0 -1
  99. package/out/src/routes/sendMessage.js +0 -451
  100. package/out/src/routes/sendMessage.js.map +0 -1
  101. package/out/src/swagger.js +0 -49
  102. package/out/src/swagger.js.map +0 -1
  103. package/out/src/types/messagePermissions.js +0 -2
  104. package/out/src/types/messagePermissions.js.map +0 -1
  105. package/out/src/types/notifications.js +0 -2
  106. package/out/src/types/notifications.js.map +0 -1
  107. package/out/src/utils/logger.js +0 -24
  108. package/out/src/utils/logger.js.map +0 -1
  109. package/out/src/utils/messagePermissions.js +0 -126
  110. package/out/src/utils/messagePermissions.js.map +0 -1
  111. package/out/src/utils/sendFCMNotification.js +0 -121
  112. package/out/src/utils/sendFCMNotification.js.map +0 -1
  113. 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
+ })