core-services-sdk 1.3.7 → 1.3.9
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/package.json +6 -5
- package/src/fastify/error-codes.js +11 -0
- package/src/http/HttpError.js +84 -10
- package/src/http/http.js +41 -31
- package/src/http/index.js +4 -0
- package/src/http/responseType.js +10 -0
- package/src/ids/index.js +2 -0
- package/src/index.js +7 -21
- package/src/mailer/transport.factory.js +28 -14
- package/src/mongodb/initialize-mongodb.js +9 -7
- package/src/rabbit-mq/index.js +1 -186
- package/src/rabbit-mq/rabbit-mq.js +189 -0
- package/src/templates/index.js +1 -0
- package/tests/fastify/error-handler.unit.test.js +39 -0
- package/tests/{with-error-handling.test.js → fastify/error-handlers/with-error-handling.test.js} +4 -3
- package/tests/http/HttpError.unit.test.js +112 -0
- package/tests/http/http-method.unit.test.js +29 -0
- package/tests/http/http.unit.test.js +167 -0
- package/tests/http/responseType.unit.test.js +45 -0
- package/tests/ids/prefixes.unit.test.js +1 -0
- package/tests/mailer/mailer.integration.test.js +95 -0
- package/tests/{mailer.unit.test.js → mailer/mailer.unit.test.js} +7 -11
- package/tests/mailer/transport.factory.unit.test.js +204 -0
- package/tests/mongodb/connect.unit.test.js +60 -0
- package/tests/mongodb/initialize-mongodb.unit.test.js +98 -0
- package/tests/mongodb/validate-mongo-uri.unit.test.js +52 -0
- package/tests/{rabbit-mq.test.js → rabbit-mq/rabbit-mq.test.js} +3 -2
- package/tests/{template-loader.integration.test.js → templates/template-loader.integration.test.js} +1 -1
- package/tests/{template-loader.unit.test.js → templates/template-loader.unit.test.js} +1 -1
- package/vitest.config.js +3 -0
- package/index.js +0 -3
- package/tests/HttpError.test.js +0 -80
- package/tests/core-util.js +0 -24
- package/tests/mailer.integration.test.js +0 -46
- package/tests/mongodb.test.js +0 -70
- package/tests/transport.factory.unit.test.js +0 -128
package/src/rabbit-mq/index.js
CHANGED
|
@@ -1,186 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { v4 as uuidv4 } from 'uuid'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {Object} Log
|
|
6
|
-
* @property {(msg: string) => void} info
|
|
7
|
-
* @property {(msg: string, ...args: any[]) => void} error
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Connects to RabbitMQ server.
|
|
12
|
-
* @param {{ host: string }} options
|
|
13
|
-
* @returns {Promise<import('amqplib').Connection>}
|
|
14
|
-
*/
|
|
15
|
-
export const connectQueueService = async ({ host }) => {
|
|
16
|
-
try {
|
|
17
|
-
return await amqp.connect(host)
|
|
18
|
-
} catch (error) {
|
|
19
|
-
console.error('Failed to connect to RabbitMQ:', error)
|
|
20
|
-
throw error
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Creates a channel from RabbitMQ connection.
|
|
26
|
-
* @param {{ host: string }} options
|
|
27
|
-
* @returns {Promise<amqp.Channel>}
|
|
28
|
-
*/
|
|
29
|
-
export const createChannel = async ({ host }) => {
|
|
30
|
-
try {
|
|
31
|
-
const connection = await connectQueueService({ host })
|
|
32
|
-
return await connection.createChannel()
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error('Failed to create channel:', error)
|
|
35
|
-
throw error
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parses a RabbitMQ message.
|
|
41
|
-
* @param {amqp.ConsumeMessage} msgInfo
|
|
42
|
-
* @returns {{ msgId: string, data: any }}
|
|
43
|
-
*/
|
|
44
|
-
const parseMessage = (msgInfo) => {
|
|
45
|
-
return JSON.parse(msgInfo.content.toString())
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Subscribes to a queue to receive messages.
|
|
50
|
-
*
|
|
51
|
-
* @param {Object} options
|
|
52
|
-
* @param {import('amqplib').Channel} options.channel - RabbitMQ channel
|
|
53
|
-
* @param {string} options.queue - Queue name to subscribe to
|
|
54
|
-
* @param {(data: any) => Promise<void>} options.onReceive - Async handler for incoming message
|
|
55
|
-
* @param {Log} options.log - Logging utility
|
|
56
|
-
* @param {boolean} [options.nackOnError=false] - Whether to nack the message on error (default: false)
|
|
57
|
-
* @param {number} [options.prefetch=1] - Max unacked messages per consumer (default: 1)
|
|
58
|
-
*
|
|
59
|
-
* @returns {Promise<void>}
|
|
60
|
-
*/
|
|
61
|
-
export const subscribeToQueue = async ({
|
|
62
|
-
log,
|
|
63
|
-
queue,
|
|
64
|
-
channel,
|
|
65
|
-
prefetch = 1,
|
|
66
|
-
onReceive,
|
|
67
|
-
nackOnError = false,
|
|
68
|
-
}) => {
|
|
69
|
-
try {
|
|
70
|
-
await channel.assertQueue(queue, { durable: true })
|
|
71
|
-
|
|
72
|
-
!!prefetch && (await channel.prefetch(prefetch))
|
|
73
|
-
|
|
74
|
-
channel.consume(queue, async (msgInfo) => {
|
|
75
|
-
if (!msgInfo) return
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const { msgId, data } = parseMessage(msgInfo)
|
|
79
|
-
log.info(`Handling message from '${queue}' msgId: ${msgId}`)
|
|
80
|
-
await onReceive(data)
|
|
81
|
-
channel.ack(msgInfo)
|
|
82
|
-
} catch (error) {
|
|
83
|
-
const { msgId } = parseMessage(msgInfo)
|
|
84
|
-
log.error(`Error handling message: ${msgId} on queue '${queue}'`)
|
|
85
|
-
log.error(error)
|
|
86
|
-
nackOnError ? channel.nack(msgInfo) : channel.ack(msgInfo)
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
} catch (error) {
|
|
90
|
-
console.error('Failed to subscribe to queue:', error)
|
|
91
|
-
throw error
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Initializes RabbitMQ integration with publish and subscribe support.
|
|
97
|
-
*
|
|
98
|
-
* @param {Object} options
|
|
99
|
-
* @param {string} options.host - RabbitMQ connection URI (e.g., 'amqp://user:pass@localhost:5672')
|
|
100
|
-
* @param {Log} options.log - Logging utility with `info()` and `error()` methods
|
|
101
|
-
*
|
|
102
|
-
* @returns {Promise<{
|
|
103
|
-
* publish: (queue: string, data: any) => Promise<boolean>,
|
|
104
|
-
* subscribe: (options: {
|
|
105
|
-
* queue: string,
|
|
106
|
-
* onReceive: (data: any) => Promise<void>,
|
|
107
|
-
* nackOnError?: boolean
|
|
108
|
-
* }) => Promise<void>,
|
|
109
|
-
* channel: amqp.Channel
|
|
110
|
-
* }>}
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* const rabbit = await initializeQueue({ host, log });
|
|
114
|
-
* await rabbit.publish('jobs', { task: 'sendEmail' });
|
|
115
|
-
* await rabbit.subscribe({
|
|
116
|
-
* queue: 'jobs',
|
|
117
|
-
* onReceive: async (data) => { console.log(data); },
|
|
118
|
-
* });
|
|
119
|
-
*/
|
|
120
|
-
export const initializeQueue = async ({ host, log }) => {
|
|
121
|
-
const channel = await createChannel({ host })
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Publishes a message to a queue.
|
|
125
|
-
* @param {string} queue
|
|
126
|
-
* @param {any} data
|
|
127
|
-
* @returns {Promise<boolean>}
|
|
128
|
-
*/
|
|
129
|
-
const publish = async (queue, data) => {
|
|
130
|
-
const msgId = uuidv4()
|
|
131
|
-
try {
|
|
132
|
-
await channel.assertQueue(queue, { durable: true })
|
|
133
|
-
log.info(`Publishing to '${queue}' msgId: ${msgId}`)
|
|
134
|
-
return channel.sendToQueue(
|
|
135
|
-
queue,
|
|
136
|
-
Buffer.from(JSON.stringify({ msgId, data })),
|
|
137
|
-
)
|
|
138
|
-
} catch (error) {
|
|
139
|
-
log.error(`Error publishing to '${queue}' msgId: ${msgId}`)
|
|
140
|
-
log.error(error)
|
|
141
|
-
throw error
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Subscribes to a queue.
|
|
147
|
-
* @param {{
|
|
148
|
-
* queue: string,
|
|
149
|
-
* onReceive: (data: any) => Promise<void>,
|
|
150
|
-
* nackOnError?: boolean
|
|
151
|
-
* }} options
|
|
152
|
-
* @returns {Promise<void>}
|
|
153
|
-
*/
|
|
154
|
-
const subscribe = async ({ queue, onReceive, nackOnError = false }) => {
|
|
155
|
-
return subscribeToQueue({ channel, queue, onReceive, log, nackOnError })
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
channel,
|
|
160
|
-
publish,
|
|
161
|
-
subscribe,
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Builds RabbitMQ URI from environment variables.
|
|
167
|
-
* @param {{
|
|
168
|
-
* RABBIT_HOST: string,
|
|
169
|
-
* RABBIT_PORT: string | number,
|
|
170
|
-
* RABBIT_USERNAME: string,
|
|
171
|
-
* RABBIT_PASSWORD: string,
|
|
172
|
-
* RABBIT_PROTOCOL?: string
|
|
173
|
-
* }} env
|
|
174
|
-
* @returns {string}
|
|
175
|
-
*/
|
|
176
|
-
export const rabbitUriFromEnv = (env) => {
|
|
177
|
-
const {
|
|
178
|
-
RABBIT_HOST,
|
|
179
|
-
RABBIT_PORT,
|
|
180
|
-
RABBIT_USERNAME,
|
|
181
|
-
RABBIT_PASSWORD,
|
|
182
|
-
RABBIT_PROTOCOL = 'amqp',
|
|
183
|
-
} = env
|
|
184
|
-
|
|
185
|
-
return `${RABBIT_PROTOCOL}://${RABBIT_USERNAME}:${RABBIT_PASSWORD}@${RABBIT_HOST}:${RABBIT_PORT}`
|
|
186
|
-
}
|
|
1
|
+
export * from './rabbit-mq.js'
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import * as amqp from 'amqplib'
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} Log
|
|
7
|
+
* @property {(msg: string) => void} info
|
|
8
|
+
* @property {(msg: string, ...args: any[]) => void} error
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Connects to RabbitMQ server.
|
|
13
|
+
* @param {{ host: string }} options
|
|
14
|
+
* @returns {Promise<amqp.Connection>}
|
|
15
|
+
*/
|
|
16
|
+
export const connectQueueService = async ({ host }) => {
|
|
17
|
+
try {
|
|
18
|
+
return await amqp.connect(host)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Failed to connect to RabbitMQ:', error)
|
|
21
|
+
throw error
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a channel from RabbitMQ connection.
|
|
27
|
+
* @param {{ host: string }} options
|
|
28
|
+
* @returns {Promise<amqp.Channel>}
|
|
29
|
+
*/
|
|
30
|
+
export const createChannel = async ({ host }) => {
|
|
31
|
+
try {
|
|
32
|
+
const connection = /** @type {amqp.Connection} */ (
|
|
33
|
+
await connectQueueService({ host })
|
|
34
|
+
)
|
|
35
|
+
return await connection.createChannel()
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to create channel:', error)
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parses a RabbitMQ message.
|
|
44
|
+
* @param {amqp.ConsumeMessage} msgInfo
|
|
45
|
+
* @returns {{ msgId: string, data: any }}
|
|
46
|
+
*/
|
|
47
|
+
const parseMessage = (msgInfo) => {
|
|
48
|
+
return JSON.parse(msgInfo.content.toString())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Subscribes to a queue to receive messages.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @param {import('amqplib').Channel} options.channel - RabbitMQ channel
|
|
56
|
+
* @param {string} options.queue - Queue name to subscribe to
|
|
57
|
+
* @param {(data: any) => Promise<void>} options.onReceive - Async handler for incoming message
|
|
58
|
+
* @param {Log} options.log - Logging utility
|
|
59
|
+
* @param {boolean} [options.nackOnError=false] - Whether to nack the message on error (default: false)
|
|
60
|
+
* @param {number} [options.prefetch=1] - Max unacked messages per consumer (default: 1)
|
|
61
|
+
*
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export const subscribeToQueue = async ({
|
|
65
|
+
log,
|
|
66
|
+
queue,
|
|
67
|
+
channel,
|
|
68
|
+
prefetch = 1,
|
|
69
|
+
onReceive,
|
|
70
|
+
nackOnError = false,
|
|
71
|
+
}) => {
|
|
72
|
+
try {
|
|
73
|
+
await channel.assertQueue(queue, { durable: true })
|
|
74
|
+
|
|
75
|
+
!!prefetch && (await channel.prefetch(prefetch))
|
|
76
|
+
|
|
77
|
+
channel.consume(queue, async (msgInfo) => {
|
|
78
|
+
if (!msgInfo) return
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const { msgId, data } = parseMessage(msgInfo)
|
|
82
|
+
log.info(`Handling message from '${queue}' msgId: ${msgId}`)
|
|
83
|
+
await onReceive(data)
|
|
84
|
+
channel.ack(msgInfo)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const { msgId } = parseMessage(msgInfo)
|
|
87
|
+
log.error(`Error handling message: ${msgId} on queue '${queue}'`)
|
|
88
|
+
log.error(error)
|
|
89
|
+
nackOnError ? channel.nack(msgInfo) : channel.ack(msgInfo)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Failed to subscribe to queue:', error)
|
|
94
|
+
throw error
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Initializes RabbitMQ integration with publish and subscribe support.
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} options
|
|
102
|
+
* @param {string} options.host - RabbitMQ connection URI (e.g., 'amqp://user:pass@localhost:5672')
|
|
103
|
+
* @param {Log} options.log - Logging utility with `info()` and `error()` methods
|
|
104
|
+
*
|
|
105
|
+
* @returns {Promise<{
|
|
106
|
+
* publish: (queue: string, data: any) => Promise<boolean>,
|
|
107
|
+
* subscribe: (options: {
|
|
108
|
+
* queue: string,
|
|
109
|
+
* onReceive: (data: any) => Promise<void>,
|
|
110
|
+
* nackOnError?: boolean
|
|
111
|
+
* }) => Promise<void>,
|
|
112
|
+
* channel: amqp.Channel
|
|
113
|
+
* }>}
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* const rabbit = await initializeQueue({ host, log });
|
|
117
|
+
* await rabbit.publish('jobs', { task: 'sendEmail' });
|
|
118
|
+
* await rabbit.subscribe({
|
|
119
|
+
* queue: 'jobs',
|
|
120
|
+
* onReceive: async (data) => { console.log(data); },
|
|
121
|
+
* });
|
|
122
|
+
*/
|
|
123
|
+
export const initializeQueue = async ({ host, log }) => {
|
|
124
|
+
const channel = await createChannel({ host })
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Publishes a message to a queue.
|
|
128
|
+
* @param {string} queue
|
|
129
|
+
* @param {any} data
|
|
130
|
+
* @returns {Promise<boolean>}
|
|
131
|
+
*/
|
|
132
|
+
const publish = async (queue, data) => {
|
|
133
|
+
const msgId = uuidv4()
|
|
134
|
+
try {
|
|
135
|
+
await channel.assertQueue(queue, { durable: true })
|
|
136
|
+
log.info(`Publishing to '${queue}' msgId: ${msgId}`)
|
|
137
|
+
return channel.sendToQueue(
|
|
138
|
+
queue,
|
|
139
|
+
Buffer.from(JSON.stringify({ msgId, data })),
|
|
140
|
+
)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
log.error(`Error publishing to '${queue}' msgId: ${msgId}`)
|
|
143
|
+
log.error(error)
|
|
144
|
+
throw error
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Subscribes to a queue.
|
|
150
|
+
* @param {{
|
|
151
|
+
* queue: string,
|
|
152
|
+
* onReceive: (data: any) => Promise<void>,
|
|
153
|
+
* nackOnError?: boolean
|
|
154
|
+
* }} options
|
|
155
|
+
* @returns {Promise<void>}
|
|
156
|
+
*/
|
|
157
|
+
const subscribe = async ({ queue, onReceive, nackOnError = false }) => {
|
|
158
|
+
return subscribeToQueue({ channel, queue, onReceive, log, nackOnError })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
channel,
|
|
163
|
+
publish,
|
|
164
|
+
subscribe,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Builds RabbitMQ URI from environment variables.
|
|
170
|
+
* @param {{
|
|
171
|
+
* RABBIT_HOST: string,
|
|
172
|
+
* RABBIT_PORT: string | number,
|
|
173
|
+
* RABBIT_USERNAME: string,
|
|
174
|
+
* RABBIT_PASSWORD: string,
|
|
175
|
+
* RABBIT_PROTOCOL?: string
|
|
176
|
+
* }} env
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
export const rabbitUriFromEnv = (env) => {
|
|
180
|
+
const {
|
|
181
|
+
RABBIT_HOST,
|
|
182
|
+
RABBIT_PORT,
|
|
183
|
+
RABBIT_USERNAME,
|
|
184
|
+
RABBIT_PASSWORD,
|
|
185
|
+
RABBIT_PROTOCOL = 'amqp',
|
|
186
|
+
} = env
|
|
187
|
+
|
|
188
|
+
return `${RABBIT_PROTOCOL}://${RABBIT_USERNAME}:${RABBIT_PASSWORD}@${RABBIT_HOST}:${RABBIT_PORT}`
|
|
189
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './template-loader.js'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import httpStatus from 'http-status'
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { GENERAL_ERROR } from '../../src/fastify/error-codes.js'
|
|
5
|
+
|
|
6
|
+
describe('GENERAL_ERROR', () => {
|
|
7
|
+
it('should have correct status code', () => {
|
|
8
|
+
expect(GENERAL_ERROR.httpStatusCode).toBe(httpStatus.INTERNAL_SERVER_ERROR)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should have correct status text', () => {
|
|
12
|
+
expect(GENERAL_ERROR.httpStatusText).toBe(
|
|
13
|
+
httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
|
|
14
|
+
)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should have correct code format', () => {
|
|
18
|
+
const expectedCode = `GENERAL.${
|
|
19
|
+
httpStatus[httpStatus.INTERNAL_SERVER_ERROR]
|
|
20
|
+
}`
|
|
21
|
+
expect(GENERAL_ERROR.code).toBe(expectedCode)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should have all required properties and types', () => {
|
|
25
|
+
expect(typeof GENERAL_ERROR.httpStatusCode).toBe('number')
|
|
26
|
+
expect(typeof GENERAL_ERROR.httpStatusText).toBe('string')
|
|
27
|
+
expect(typeof GENERAL_ERROR.code).toBe('string')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should match full expected structure', () => {
|
|
31
|
+
const expected = {
|
|
32
|
+
httpStatusCode: 500,
|
|
33
|
+
httpStatusText: 'Internal Server Error',
|
|
34
|
+
code: 'GENERAL.Internal Server Error',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect(GENERAL_ERROR).toEqual(expected)
|
|
38
|
+
})
|
|
39
|
+
})
|
package/tests/{with-error-handling.test.js → fastify/error-handlers/with-error-handling.test.js}
RENAMED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import httpStatus from 'http-status'
|
|
2
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
4
|
|
|
4
|
-
import { HttpError } from '
|
|
5
|
-
import { GENERAL_ERROR } from '
|
|
5
|
+
import { HttpError } from '../../../src/http/HttpError.js'
|
|
6
|
+
import { GENERAL_ERROR } from '../../../src/fastify/error-codes.js'
|
|
6
7
|
import {
|
|
7
8
|
replyOnErrorOnly,
|
|
8
9
|
withErrorHandling,
|
|
9
10
|
withErrorHandlingReply,
|
|
10
|
-
} from '
|
|
11
|
+
} from '../../../src/fastify/error-handlers/with-error-handling.js'
|
|
11
12
|
|
|
12
13
|
describe('withErrorHandling', () => {
|
|
13
14
|
const log = { error: vi.fn() }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import httpStatus from 'http-status'
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { HttpError } from '../../src/http/HttpError.js'
|
|
5
|
+
|
|
6
|
+
describe('HttpError', () => {
|
|
7
|
+
it('should create an instance with custom message, code, and extendInfo', () => {
|
|
8
|
+
const error = new HttpError({
|
|
9
|
+
code: 'INVALID_INPUT',
|
|
10
|
+
message: 'Invalid input provided',
|
|
11
|
+
httpStatusCode: 400,
|
|
12
|
+
extendInfo: { field: 'email', reason: 'missing' },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(error).toBeInstanceOf(HttpError)
|
|
16
|
+
expect(error.message).toBe('Invalid input provided')
|
|
17
|
+
expect(error.code).toBe('INVALID_INPUT')
|
|
18
|
+
expect(error.httpStatusCode).toBe(400)
|
|
19
|
+
expect(error.httpStatusText).toBe(httpStatus[400])
|
|
20
|
+
expect(error.extendInfo).toEqual({ field: 'email', reason: 'missing' })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should fallback to default message from status if message is missing', () => {
|
|
24
|
+
const error = new HttpError({
|
|
25
|
+
code: 'BAD_REQUEST',
|
|
26
|
+
httpStatusCode: 400,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(error.message).toBe(httpStatus[400])
|
|
30
|
+
expect(error.httpStatusText).toBe(httpStatus[400])
|
|
31
|
+
expect(error.extendInfo).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should fallback to code as message if message and status code missing', () => {
|
|
35
|
+
const error = new HttpError({ code: 'ERROR_CODE_ONLY' })
|
|
36
|
+
|
|
37
|
+
expect(error.message).toBe('ERROR_CODE_ONLY')
|
|
38
|
+
expect(error.httpStatusCode).toBeUndefined()
|
|
39
|
+
expect(error.extendInfo).toBeUndefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should fallback to "Unknown error" if no data provided', () => {
|
|
43
|
+
const error = new HttpError()
|
|
44
|
+
|
|
45
|
+
expect(error.message).toBe('Unknown error')
|
|
46
|
+
expect(error.extendInfo).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should return correct JSON representation without extendInfo', () => {
|
|
50
|
+
const error = new HttpError({
|
|
51
|
+
code: 'NOT_FOUND',
|
|
52
|
+
message: 'Resource not found',
|
|
53
|
+
httpStatusCode: 404,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
expect(error.toJSON()).toEqual({
|
|
57
|
+
code: 'NOT_FOUND',
|
|
58
|
+
message: 'Resource not found',
|
|
59
|
+
httpStatusCode: 404,
|
|
60
|
+
httpStatusText: httpStatus[404],
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should return correct JSON representation with extendInfo', () => {
|
|
65
|
+
const error = new HttpError({
|
|
66
|
+
code: 'NOT_FOUND',
|
|
67
|
+
message: 'Resource not found',
|
|
68
|
+
httpStatusCode: 404,
|
|
69
|
+
extendInfo: { resource: 'user', id: 123 },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(error.toJSON()).toEqual({
|
|
73
|
+
code: 'NOT_FOUND',
|
|
74
|
+
message: 'Resource not found',
|
|
75
|
+
httpStatusCode: 404,
|
|
76
|
+
httpStatusText: httpStatus[404],
|
|
77
|
+
extendInfo: { resource: 'user', id: 123 },
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should detect instance using isHttpError', () => {
|
|
82
|
+
const error = new HttpError({
|
|
83
|
+
code: 'TEST',
|
|
84
|
+
httpStatusCode: 500,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(HttpError.isHttpError(error)).toBe(true)
|
|
88
|
+
expect(HttpError.isHttpError(new Error('x'))).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('FromError should return same instance if already HttpError', () => {
|
|
92
|
+
const original = new HttpError({
|
|
93
|
+
code: 'ALREADY_HTTP',
|
|
94
|
+
httpStatusCode: 401,
|
|
95
|
+
})
|
|
96
|
+
const result = HttpError.FromError(original)
|
|
97
|
+
|
|
98
|
+
expect(result).toBe(original)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('FromError should wrap generic Error', () => {
|
|
102
|
+
const err = new Error('Boom!')
|
|
103
|
+
const httpError = HttpError.FromError(err)
|
|
104
|
+
|
|
105
|
+
expect(httpError).toBeInstanceOf(HttpError)
|
|
106
|
+
expect(httpError.message).toBe('Boom!')
|
|
107
|
+
expect(httpError.code).toBe('UNHANDLED_ERROR')
|
|
108
|
+
expect(httpError.httpStatusCode).toBe(500)
|
|
109
|
+
expect(httpError.httpStatusText).toBe(httpStatus[500])
|
|
110
|
+
expect(httpError.extendInfo).toBeUndefined()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { HTTP_METHODS } from '../../src/http/http-method.js'
|
|
5
|
+
|
|
6
|
+
describe('HTTP_METHODS constant', () => {
|
|
7
|
+
it('should include all standard HTTP methods', () => {
|
|
8
|
+
expect(HTTP_METHODS).toEqual({
|
|
9
|
+
GET: 'GET',
|
|
10
|
+
POST: 'POST',
|
|
11
|
+
PUT: 'PUT',
|
|
12
|
+
PATCH: 'PATCH',
|
|
13
|
+
DELETE: 'DELETE',
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should be frozen (immutable)', () => {
|
|
18
|
+
expect(Object.isFrozen(HTTP_METHODS)).toBe(true)
|
|
19
|
+
|
|
20
|
+
// Attempt to mutate and verify it fails
|
|
21
|
+
try {
|
|
22
|
+
HTTP_METHODS.NEW = 'NEW'
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// Ignore in strict mode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
expect(HTTP_METHODS).not.toHaveProperty('NEW')
|
|
28
|
+
})
|
|
29
|
+
})
|