core-services-sdk 1.3.7 → 1.3.8
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 +3 -2
- 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 +19 -6
- 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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer'
|
|
2
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { Mailer } from '../../src/mailer/mailer.service.js'
|
|
5
|
+
|
|
6
|
+
describe('Mailer (integration)', () => {
|
|
7
|
+
let transporter
|
|
8
|
+
let mailer
|
|
9
|
+
let testAccount
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
testAccount = await nodemailer.createTestAccount()
|
|
13
|
+
|
|
14
|
+
transporter = nodemailer.createTransport({
|
|
15
|
+
host: testAccount.smtp.host,
|
|
16
|
+
port: testAccount.smtp.port,
|
|
17
|
+
secure: testAccount.smtp.secure,
|
|
18
|
+
auth: {
|
|
19
|
+
user: testAccount.user,
|
|
20
|
+
pass: testAccount.pass,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
mailer = new Mailer(transporter)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should send email with HTML + text', async () => {
|
|
28
|
+
const result = await mailer.send({
|
|
29
|
+
to: 'recipient@example.com',
|
|
30
|
+
from: '"My App" <no-reply@example.com>',
|
|
31
|
+
subject: 'Integration Test: HTML + text',
|
|
32
|
+
text: 'Hello from plain text',
|
|
33
|
+
html: '<strong>Hello HTML</strong>',
|
|
34
|
+
cc: '',
|
|
35
|
+
bcc: '',
|
|
36
|
+
replyTo: '',
|
|
37
|
+
attachments: '',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(result.messageId).toBeDefined()
|
|
41
|
+
expect(result.envelope).toBeDefined()
|
|
42
|
+
expect(result.accepted).toContain('recipient@example.com')
|
|
43
|
+
|
|
44
|
+
const previewUrl = nodemailer.getTestMessageUrl(result)
|
|
45
|
+
console.log(`📨 Preview (HTML+text): ${previewUrl}`)
|
|
46
|
+
expect(previewUrl).toMatch(/^https:\/\/ethereal\.email/)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should send HTML-only email', async () => {
|
|
50
|
+
const result = await mailer.send({
|
|
51
|
+
to: 'recipient2@example.com',
|
|
52
|
+
from: '"My App" <no-reply@example.com>',
|
|
53
|
+
subject: 'Integration Test: HTML only',
|
|
54
|
+
html: '<h2>Only HTML</h2>',
|
|
55
|
+
cc: '',
|
|
56
|
+
bcc: '',
|
|
57
|
+
text: '',
|
|
58
|
+
replyTo: '',
|
|
59
|
+
attachments: '',
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(result.messageId).toBeDefined()
|
|
63
|
+
expect(result.accepted).toContain('recipient2@example.com')
|
|
64
|
+
|
|
65
|
+
const previewUrl = nodemailer.getTestMessageUrl(result)
|
|
66
|
+
console.log(`📨 Preview (HTML only): ${previewUrl}`)
|
|
67
|
+
expect(previewUrl).toMatch(/^https:\/\/ethereal\.email/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should send email with attachment', async () => {
|
|
71
|
+
const result = await mailer.send({
|
|
72
|
+
to: 'recipient3@example.com',
|
|
73
|
+
from: '"My App" <no-reply@example.com>',
|
|
74
|
+
subject: 'Integration Test: With Attachment',
|
|
75
|
+
text: 'See attached file.',
|
|
76
|
+
html: '<p>See attached file.</p>',
|
|
77
|
+
attachments: [
|
|
78
|
+
{
|
|
79
|
+
filename: 'test.txt',
|
|
80
|
+
content: 'This is the content of the attachment.',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
cc: '',
|
|
84
|
+
bcc: '',
|
|
85
|
+
replyTo: '',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(result.messageId).toBeDefined()
|
|
89
|
+
expect(result.accepted).toContain('recipient3@example.com')
|
|
90
|
+
|
|
91
|
+
const previewUrl = nodemailer.getTestMessageUrl(result)
|
|
92
|
+
console.log(`📨 Preview (With Attachment): ${previewUrl}`)
|
|
93
|
+
expect(previewUrl).toMatch(/^https:\/\/ethereal\.email/)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -4,9 +4,9 @@ const mockTransport = {
|
|
|
4
4
|
sendMail: vi.fn(),
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
vi.mock('
|
|
9
|
-
const actual = await vi.importActual('
|
|
7
|
+
// Mock the TransportFactory with a spy
|
|
8
|
+
vi.mock('../../src/mailer/transport.factory.js', async () => {
|
|
9
|
+
const actual = await vi.importActual('../../src/mailer/transport.factory.js') // fixed path
|
|
10
10
|
return {
|
|
11
11
|
...actual,
|
|
12
12
|
TransportFactory: {
|
|
@@ -16,17 +16,16 @@ vi.mock('../src/mailer/transport.factory.js', async () => {
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
// Mock Mailer class
|
|
19
|
-
vi.mock('
|
|
19
|
+
vi.mock('../../src/mailer/mailer.service.js', async () => {
|
|
20
20
|
const Mailer = vi.fn().mockImplementation((transport) => ({
|
|
21
21
|
send: vi.fn((opts) => transport.sendMail(opts)),
|
|
22
22
|
}))
|
|
23
23
|
return { Mailer }
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
import { TransportFactory } from '
|
|
29
|
-
import { Mailer } from '../src/mailer/mailer.service.js'
|
|
26
|
+
import { initMailer } from '../../src/mailer/index.js'
|
|
27
|
+
import { Mailer } from '../../src/mailer/mailer.service.js'
|
|
28
|
+
import { TransportFactory } from '../../src/mailer/transport.factory.js'
|
|
30
29
|
|
|
31
30
|
describe('initMailer', () => {
|
|
32
31
|
beforeEach(() => {
|
|
@@ -44,10 +43,7 @@ describe('initMailer', () => {
|
|
|
44
43
|
|
|
45
44
|
const mailer = initMailer(config)
|
|
46
45
|
|
|
47
|
-
// Ensure TransportFactory.create was called
|
|
48
46
|
expect(TransportFactory.create).toHaveBeenCalledWith(config)
|
|
49
|
-
|
|
50
|
-
// Ensure Mailer was created with the mock transport
|
|
51
47
|
expect(Mailer).toHaveBeenCalledWith(mockTransport)
|
|
52
48
|
|
|
53
49
|
const emailOptions = {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer'
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
+
|
|
4
|
+
// ===================
|
|
5
|
+
// SAFELY MOCK MODULES
|
|
6
|
+
// ===================
|
|
7
|
+
|
|
8
|
+
vi.mock('nodemailer', () => {
|
|
9
|
+
const createTransport = vi.fn((options) => ({
|
|
10
|
+
type: 'mock-transport',
|
|
11
|
+
options,
|
|
12
|
+
}))
|
|
13
|
+
return {
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: { createTransport },
|
|
16
|
+
createTransport,
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
vi.mock('nodemailer-sendgrid-transport', () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
default: vi.fn((opts) => ({
|
|
23
|
+
sendgrid: true,
|
|
24
|
+
options: opts,
|
|
25
|
+
})),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
vi.mock('@aws-sdk/client-ses', () => {
|
|
29
|
+
const mockSesInstance = { mocked: true } // consistent reference
|
|
30
|
+
const sesClient = vi.fn(() => mockSesInstance)
|
|
31
|
+
|
|
32
|
+
globalThis.__mockedSesClient__ = sesClient
|
|
33
|
+
globalThis.__mockedSesInstance__ = mockSesInstance
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
SESClient: sesClient,
|
|
37
|
+
SendRawEmailCommand: 'mocked-command',
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
vi.mock('@aws-sdk/credential-provider-node', () => {
|
|
42
|
+
const defaultProvider = vi.fn(() => 'mocked-default-provider')
|
|
43
|
+
globalThis.__mockedDefaultProvider__ = defaultProvider
|
|
44
|
+
return {
|
|
45
|
+
defaultProvider,
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// ==========================
|
|
50
|
+
// NOW IMPORT MODULE UNDER TEST
|
|
51
|
+
// ==========================
|
|
52
|
+
|
|
53
|
+
import { TransportFactory } from '../../src/mailer/transport.factory.js'
|
|
54
|
+
|
|
55
|
+
describe('TransportFactory', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should create smtp transport', () => {
|
|
61
|
+
const config = {
|
|
62
|
+
type: 'smtp',
|
|
63
|
+
host: 'smtp.example.com',
|
|
64
|
+
port: 587,
|
|
65
|
+
secure: false,
|
|
66
|
+
auth: { user: 'user', pass: 'pass' },
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
const transport = TransportFactory.create(config)
|
|
71
|
+
|
|
72
|
+
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
|
73
|
+
host: config.host,
|
|
74
|
+
port: config.port,
|
|
75
|
+
secure: config.secure,
|
|
76
|
+
auth: config.auth,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
expect(transport.type).toBe('mock-transport')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should create gmail transport', () => {
|
|
84
|
+
const config = {
|
|
85
|
+
type: 'gmail',
|
|
86
|
+
auth: { user: 'user@gmail.com', pass: 'pass' },
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
const transport = TransportFactory.create(config)
|
|
91
|
+
|
|
92
|
+
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
|
93
|
+
service: 'gmail',
|
|
94
|
+
auth: config.auth,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
expect(transport.type).toBe('mock-transport')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should create sendgrid transport', () => {
|
|
102
|
+
const config = {
|
|
103
|
+
type: 'sendgrid',
|
|
104
|
+
apiKey: 'SG.xxxx',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// @ts-ignore
|
|
108
|
+
const transport = TransportFactory.create(config)
|
|
109
|
+
|
|
110
|
+
// @ts-ignore
|
|
111
|
+
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
112
|
+
|
|
113
|
+
expect(args).toEqual({
|
|
114
|
+
sendgrid: true,
|
|
115
|
+
options: {
|
|
116
|
+
auth: { api_key: config.apiKey },
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(transport).toEqual({
|
|
121
|
+
type: 'mock-transport',
|
|
122
|
+
options: {
|
|
123
|
+
sendgrid: true,
|
|
124
|
+
options: {
|
|
125
|
+
auth: { api_key: config.apiKey },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should create ses transport with credentials', () => {
|
|
132
|
+
const config = {
|
|
133
|
+
type: 'ses',
|
|
134
|
+
accessKeyId: 'AKIA...',
|
|
135
|
+
secretAccessKey: 'secret',
|
|
136
|
+
region: 'us-west-2',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
const transport = TransportFactory.create(config)
|
|
141
|
+
|
|
142
|
+
expect(globalThis.__mockedSesClient__).toHaveBeenCalledWith({
|
|
143
|
+
region: config.region,
|
|
144
|
+
credentials: {
|
|
145
|
+
accessKeyId: config.accessKeyId,
|
|
146
|
+
secretAccessKey: config.secretAccessKey,
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// @ts-ignore
|
|
151
|
+
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
152
|
+
|
|
153
|
+
expect(args).toEqual({
|
|
154
|
+
SES: {
|
|
155
|
+
ses: globalThis.__mockedSesInstance__,
|
|
156
|
+
aws: {
|
|
157
|
+
SendRawEmailCommand: 'mocked-command',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// @ts-ignore
|
|
163
|
+
expect(transport.type).toBe('mock-transport')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should create ses transport with defaultProvider fallback', () => {
|
|
167
|
+
const config = {
|
|
168
|
+
type: 'ses',
|
|
169
|
+
region: 'us-east-1',
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// @ts-ignore
|
|
173
|
+
const transport = TransportFactory.create(config)
|
|
174
|
+
|
|
175
|
+
expect(globalThis.__mockedDefaultProvider__).toHaveBeenCalled()
|
|
176
|
+
|
|
177
|
+
expect(globalThis.__mockedSesClient__).toHaveBeenCalledWith({
|
|
178
|
+
region: config.region,
|
|
179
|
+
credentials: 'mocked-default-provider',
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
184
|
+
|
|
185
|
+
expect(args).toEqual({
|
|
186
|
+
SES: {
|
|
187
|
+
ses: globalThis.__mockedSesInstance__,
|
|
188
|
+
aws: {
|
|
189
|
+
SendRawEmailCommand: 'mocked-command',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// @ts-ignore
|
|
195
|
+
expect(transport.type).toBe('mock-transport')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should throw error for unsupported type', () => {
|
|
199
|
+
expect(() => {
|
|
200
|
+
// @ts-ignore
|
|
201
|
+
TransportFactory.create({ type: 'invalid' })
|
|
202
|
+
}).toThrow('Unsupported transport type: invalid')
|
|
203
|
+
})
|
|
204
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { MongoClient, ServerApiVersion } from 'mongodb'
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { mongoConnect } from '../../src/mongodb/connect.js'
|
|
6
|
+
|
|
7
|
+
vi.mock('mongodb', async () => {
|
|
8
|
+
const actual = await vi.importActual('mongodb')
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
MongoClient: {
|
|
12
|
+
connect: vi.fn(),
|
|
13
|
+
},
|
|
14
|
+
ServerApiVersion: actual.ServerApiVersion, // use real enum
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('mongoConnect', () => {
|
|
19
|
+
const fakeClient = { db: vi.fn() }
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks()
|
|
23
|
+
MongoClient.connect.mockResolvedValue(fakeClient)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should call MongoClient.connect with default serverApi options', async () => {
|
|
27
|
+
const uri = 'mongodb://localhost:27017'
|
|
28
|
+
await mongoConnect({ uri })
|
|
29
|
+
|
|
30
|
+
expect(MongoClient.connect).toHaveBeenCalledWith(uri, {
|
|
31
|
+
serverApi: {
|
|
32
|
+
version: ServerApiVersion.v1,
|
|
33
|
+
strict: true,
|
|
34
|
+
deprecationErrors: true,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should merge custom serverApi options', async () => {
|
|
40
|
+
const uri = 'mongodb://localhost:27017'
|
|
41
|
+
const serverApi = { strict: false }
|
|
42
|
+
|
|
43
|
+
await mongoConnect({ uri, serverApi })
|
|
44
|
+
|
|
45
|
+
expect(MongoClient.connect).toHaveBeenCalledWith(uri, {
|
|
46
|
+
serverApi: {
|
|
47
|
+
version: ServerApiVersion.v1,
|
|
48
|
+
strict: false,
|
|
49
|
+
deprecationErrors: true,
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should return the connected client', async () => {
|
|
55
|
+
const uri = 'mongodb://localhost:27017'
|
|
56
|
+
const client = await mongoConnect({ uri })
|
|
57
|
+
|
|
58
|
+
expect(client).toBe(fakeClient)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { initializeMongoDb } from '../../src/mongodb/initialize-mongodb.js'
|
|
5
|
+
|
|
6
|
+
// Shared session spy
|
|
7
|
+
const sessionSpy = {
|
|
8
|
+
startTransaction: vi.fn(),
|
|
9
|
+
commitTransaction: vi.fn(),
|
|
10
|
+
abortTransaction: vi.fn(),
|
|
11
|
+
endSession: vi.fn(),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ✅ Mock path must match actual import in initializeMongoDb
|
|
15
|
+
vi.mock('../../src/mongodb/connect.js', () => {
|
|
16
|
+
const fakeCollection = (name) => ({
|
|
17
|
+
name,
|
|
18
|
+
insertOne: vi.fn(),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const fakeDb = {
|
|
22
|
+
collection: vi.fn((name) => fakeCollection(name)),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fakeClient = {
|
|
26
|
+
db: vi.fn(() => fakeDb),
|
|
27
|
+
startSession: () => sessionSpy,
|
|
28
|
+
close: vi.fn(),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
mongoConnect: vi.fn(async () => fakeClient),
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
import { mongoConnect } from '../../src/mongodb/connect.js'
|
|
37
|
+
|
|
38
|
+
describe('initializeMongoDb', () => {
|
|
39
|
+
const config = {
|
|
40
|
+
uri: 'mongodb://localhost:27017',
|
|
41
|
+
options: { dbName: 'testdb' },
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const collectionNames = {
|
|
45
|
+
users: 'users_collection',
|
|
46
|
+
logs: 'logs_collection',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should initialize collections correctly', async () => {
|
|
54
|
+
const db = await initializeMongoDb({ config, collectionNames })
|
|
55
|
+
|
|
56
|
+
expect(mongoConnect).toHaveBeenCalledWith(config)
|
|
57
|
+
expect(db.users.name).toBe('users_collection')
|
|
58
|
+
expect(db.logs.name).toBe('logs_collection')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should expose the mongo client via getter', async () => {
|
|
62
|
+
const db = await initializeMongoDb({ config, collectionNames })
|
|
63
|
+
expect(db.client).toBeDefined()
|
|
64
|
+
expect(db.client.db).toBeTypeOf('function')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should run actions within a transaction and commit on success', async () => {
|
|
68
|
+
const db = await initializeMongoDb({ config, collectionNames })
|
|
69
|
+
|
|
70
|
+
const actionMock = vi.fn(async ({ session }) => {
|
|
71
|
+
expect(session).toBe(sessionSpy)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await db.withTransaction(actionMock)
|
|
75
|
+
|
|
76
|
+
expect(sessionSpy.startTransaction).toHaveBeenCalled()
|
|
77
|
+
expect(sessionSpy.commitTransaction).toHaveBeenCalled()
|
|
78
|
+
expect(sessionSpy.abortTransaction).not.toHaveBeenCalled()
|
|
79
|
+
expect(sessionSpy.endSession).toHaveBeenCalled()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should abort transaction on error', async () => {
|
|
83
|
+
const db = await initializeMongoDb({ config, collectionNames })
|
|
84
|
+
|
|
85
|
+
const actionMock = vi.fn(async () => {
|
|
86
|
+
throw new Error('Intentional failure')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await expect(db.withTransaction(actionMock)).rejects.toThrow(
|
|
90
|
+
'Intentional failure',
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
expect(sessionSpy.startTransaction).toHaveBeenCalled()
|
|
94
|
+
expect(sessionSpy.commitTransaction).not.toHaveBeenCalled()
|
|
95
|
+
expect(sessionSpy.abortTransaction).toHaveBeenCalled()
|
|
96
|
+
expect(sessionSpy.endSession).toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { isValidMongoUri } from '../../src/mongodb/validate-mongo-uri.js'
|
|
5
|
+
|
|
6
|
+
describe('isValidMongoUri', () => {
|
|
7
|
+
it('should return true for a valid mongodb:// URI', () => {
|
|
8
|
+
expect(isValidMongoUri('mongodb://localhost:27017/mydb')).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should return true for a valid mongodb+srv:// URI', () => {
|
|
12
|
+
expect(isValidMongoUri('mongodb+srv://cluster.example.com/test')).toBe(true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should return false for non-MongoDB protocol (http)', () => {
|
|
16
|
+
expect(isValidMongoUri('http://localhost')).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return false for empty string', () => {
|
|
20
|
+
expect(isValidMongoUri('')).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should return false for whitespace string', () => {
|
|
24
|
+
expect(isValidMongoUri(' ')).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should return false for non-string input', () => {
|
|
28
|
+
expect(isValidMongoUri(null)).toBe(false)
|
|
29
|
+
expect(isValidMongoUri(undefined)).toBe(false)
|
|
30
|
+
expect(isValidMongoUri(12345)).toBe(false)
|
|
31
|
+
expect(isValidMongoUri({})).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should return false for malformed URI', () => {
|
|
35
|
+
expect(isValidMongoUri('mongodb://')).toBe(false)
|
|
36
|
+
expect(isValidMongoUri('mongodb+srv://')).toBe(false)
|
|
37
|
+
expect(isValidMongoUri('mongodb:/localhost')).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should allow valid URIs with credentials and options', () => {
|
|
41
|
+
expect(
|
|
42
|
+
isValidMongoUri(
|
|
43
|
+
'mongodb://user:pass@localhost:27017/mydb?retryWrites=true',
|
|
44
|
+
),
|
|
45
|
+
).toBe(true)
|
|
46
|
+
expect(
|
|
47
|
+
isValidMongoUri(
|
|
48
|
+
'mongodb+srv://user:pass@cluster0.example.mongodb.net/mydb',
|
|
49
|
+
),
|
|
50
|
+
).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll } from 'vitest'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { initializeQueue, rabbitUriFromEnv } from '../../src/rabbit-mq/index.js'
|
|
3
4
|
|
|
4
5
|
const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
|
|
5
6
|
|
|
@@ -37,8 +38,8 @@ describe('RabbitMQ SDK', () => {
|
|
|
37
38
|
describe('rabbitUriFromEnv', () => {
|
|
38
39
|
it('should build valid amqp URI from env', () => {
|
|
39
40
|
const env = {
|
|
40
|
-
RABBIT_HOST: '0.0.0.0',
|
|
41
41
|
RABBIT_PORT: 5672,
|
|
42
|
+
RABBIT_HOST: '0.0.0.0',
|
|
42
43
|
RABBIT_USERNAME: 'botq',
|
|
43
44
|
RABBIT_PASSWORD: 'botq',
|
|
44
45
|
_RABBIT_HOST: '0.0.0.0',
|
package/tests/{template-loader.integration.test.js → templates/template-loader.integration.test.js}
RENAMED
|
@@ -3,7 +3,7 @@ import { tmpdir } from 'os'
|
|
|
3
3
|
import { writeFile, rm, mkdir } from 'fs/promises'
|
|
4
4
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
5
5
|
|
|
6
|
-
import { loadTemplates } from '
|
|
6
|
+
import { loadTemplates } from '../../src/templates/template-loader.js'
|
|
7
7
|
|
|
8
8
|
const tempDir = join(tmpdir(), 'template-tests')
|
|
9
9
|
|
|
@@ -2,7 +2,7 @@ import dot from 'dot'
|
|
|
2
2
|
import * as fs from 'fs/promises'
|
|
3
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { loadTemplates } from '
|
|
5
|
+
import { loadTemplates } from '../../src/templates/template-loader.js'
|
|
6
6
|
|
|
7
7
|
vi.mock('fs/promises')
|
|
8
8
|
|
package/vitest.config.js
CHANGED
package/index.js
DELETED
package/tests/HttpError.test.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { HttpError } from '../src/http/HttpError.js'
|
|
3
|
-
import httpStatus from 'http-status'
|
|
4
|
-
|
|
5
|
-
describe('HttpError', () => {
|
|
6
|
-
it('creates an error with all fields explicitly set', () => {
|
|
7
|
-
const err = new HttpError({
|
|
8
|
-
code: 'SOME_ERROR',
|
|
9
|
-
message: 'Something went wrong',
|
|
10
|
-
httpStatusCode: httpStatus.BAD_REQUEST,
|
|
11
|
-
httpStatusText: httpStatus[httpStatus.BAD_REQUEST],
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
expect(err).toBeInstanceOf(HttpError)
|
|
15
|
-
expect(err.message).toBe('Something went wrong')
|
|
16
|
-
expect(err.code).toBe('SOME_ERROR')
|
|
17
|
-
expect(err.httpStatusCode).toBe(httpStatus.BAD_REQUEST)
|
|
18
|
-
expect(err.httpStatusText).toBe(httpStatus[httpStatus.BAD_REQUEST])
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('falls back to code if message and httpStatusCode are missing', () => {
|
|
22
|
-
const err = new HttpError({ code: 'MY_CODE' })
|
|
23
|
-
expect(err.message).toBe('MY_CODE')
|
|
24
|
-
expect(err.code).toBe('MY_CODE')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('falls back to status text if only httpStatusCode is provided', () => {
|
|
28
|
-
const err = new HttpError({ httpStatusCode: httpStatus.NOT_FOUND })
|
|
29
|
-
expect(err.message).toBe(httpStatus[httpStatus.NOT_FOUND])
|
|
30
|
-
expect(err.httpStatusText).toBe(httpStatus[httpStatus.NOT_FOUND])
|
|
31
|
-
expect(err.httpStatusCode).toBe(httpStatus.NOT_FOUND)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('falls back to "Unknown error" if nothing is provided', () => {
|
|
35
|
-
const err = new HttpError()
|
|
36
|
-
expect(err.message).toBe('Unknown error')
|
|
37
|
-
expect(err.code).toBeUndefined()
|
|
38
|
-
expect(err.httpStatusCode).toBeUndefined()
|
|
39
|
-
expect(err.httpStatusText).toBeUndefined()
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('uses httpStatusText if not explicitly set but httpStatusCode is present', () => {
|
|
43
|
-
const err = new HttpError({ httpStatusCode: httpStatus.UNAUTHORIZED })
|
|
44
|
-
expect(err.httpStatusText).toBe(httpStatus[httpStatus.UNAUTHORIZED])
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('supports static isInstanceOf check', () => {
|
|
48
|
-
const err = new HttpError()
|
|
49
|
-
expect(HttpError.isInstanceOf(err)).toBe(true)
|
|
50
|
-
expect(HttpError.isInstanceOf(new Error())).toBe(false)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('supports Symbol.hasInstance (dynamic instanceof)', () => {
|
|
54
|
-
const fake = {
|
|
55
|
-
code: 'FAKE',
|
|
56
|
-
message: 'Fake error',
|
|
57
|
-
httpStatusCode: httpStatus.UNAUTHORIZED,
|
|
58
|
-
httpStatusText: httpStatus[httpStatus.UNAUTHORIZED],
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
expect(fake instanceof HttpError).toBe(true)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('serializes correctly via toJSON', () => {
|
|
65
|
-
const err = new HttpError({
|
|
66
|
-
code: 'TOO_FAST',
|
|
67
|
-
message: 'You are too fast',
|
|
68
|
-
httpStatusCode: httpStatus.TOO_MANY_REQUESTS,
|
|
69
|
-
httpStatusText: httpStatus[httpStatus.TOO_MANY_REQUESTS],
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
const json = JSON.stringify(err)
|
|
73
|
-
expect(JSON.parse(json)).toEqual({
|
|
74
|
-
code: 'TOO_FAST',
|
|
75
|
-
message: 'You are too fast',
|
|
76
|
-
httpStatusCode: httpStatus.TOO_MANY_REQUESTS,
|
|
77
|
-
httpStatusText: httpStatus[httpStatus.TOO_MANY_REQUESTS],
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
})
|