odac 0.9.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/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- package/web/view/header/main.html +14 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test utilities for common assertions and setup patterns
|
|
3
|
+
* Provides helper functions to reduce boilerplate in server tests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {mockCandy, mockLangGet} = require('./globalCandy')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sets up the global Candy mock and __ function for tests
|
|
10
|
+
* Should be called in beforeEach or beforeAll
|
|
11
|
+
*/
|
|
12
|
+
const setupGlobalMocks = () => {
|
|
13
|
+
// Set up global Candy mock
|
|
14
|
+
global.Candy = mockCandy
|
|
15
|
+
|
|
16
|
+
// Set up global __ function mock
|
|
17
|
+
global.__ = mockLangGet
|
|
18
|
+
|
|
19
|
+
// Clear all previous mock calls
|
|
20
|
+
mockCandy.clearMocks()
|
|
21
|
+
mockLangGet.mockClear()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cleans up global mocks after tests
|
|
26
|
+
* Should be called in afterEach or afterAll
|
|
27
|
+
*/
|
|
28
|
+
const cleanupGlobalMocks = () => {
|
|
29
|
+
mockCandy.resetMocks()
|
|
30
|
+
mockLangGet.mockClear()
|
|
31
|
+
|
|
32
|
+
// Reset global references
|
|
33
|
+
delete global.Candy
|
|
34
|
+
delete global.__
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a mock timer that can be controlled in tests
|
|
39
|
+
*/
|
|
40
|
+
const createMockTimer = () => {
|
|
41
|
+
const timer = {
|
|
42
|
+
id: Math.floor(Math.random() * 1000),
|
|
43
|
+
callback: null,
|
|
44
|
+
interval: 0,
|
|
45
|
+
active: false,
|
|
46
|
+
unref: jest.fn(() => timer)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return timer
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Helper to mock setTimeout and setInterval
|
|
54
|
+
*/
|
|
55
|
+
const mockTimers = () => {
|
|
56
|
+
const timers = new Map()
|
|
57
|
+
let timerId = 1
|
|
58
|
+
|
|
59
|
+
const mockSetTimeout = jest.fn((callback, delay) => {
|
|
60
|
+
const id = timerId++
|
|
61
|
+
timers.set(id, {callback, delay, type: 'timeout'})
|
|
62
|
+
return id
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const mockSetInterval = jest.fn((callback, delay) => {
|
|
66
|
+
const id = timerId++
|
|
67
|
+
const timer = {callback, delay, type: 'interval', unref: jest.fn()}
|
|
68
|
+
timers.set(id, timer)
|
|
69
|
+
return timer
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const mockClearTimeout = jest.fn(id => {
|
|
73
|
+
timers.delete(id)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const mockClearInterval = jest.fn(id => {
|
|
77
|
+
timers.delete(id)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Helper to execute timer callbacks
|
|
81
|
+
const executeTimer = id => {
|
|
82
|
+
const timer = timers.get(id)
|
|
83
|
+
if (timer && timer.callback) {
|
|
84
|
+
timer.callback()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Helper to execute all timers
|
|
89
|
+
const executeAllTimers = () => {
|
|
90
|
+
timers.forEach((timer, id) => {
|
|
91
|
+
if (timer.callback) {
|
|
92
|
+
timer.callback()
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
setTimeout: mockSetTimeout,
|
|
99
|
+
setInterval: mockSetInterval,
|
|
100
|
+
clearTimeout: mockClearTimeout,
|
|
101
|
+
clearInterval: mockClearInterval,
|
|
102
|
+
executeTimer,
|
|
103
|
+
executeAllTimers,
|
|
104
|
+
getTimers: () => timers
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Helper to create a mock EventEmitter
|
|
110
|
+
*/
|
|
111
|
+
const createMockEventEmitter = () => {
|
|
112
|
+
const listeners = new Map()
|
|
113
|
+
|
|
114
|
+
const emitter = {
|
|
115
|
+
on: jest.fn((event, listener) => {
|
|
116
|
+
if (!listeners.has(event)) {
|
|
117
|
+
listeners.set(event, [])
|
|
118
|
+
}
|
|
119
|
+
listeners.get(event).push(listener)
|
|
120
|
+
return emitter
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
once: jest.fn((event, listener) => {
|
|
124
|
+
const onceWrapper = (...args) => {
|
|
125
|
+
listener(...args)
|
|
126
|
+
emitter.removeListener(event, onceWrapper)
|
|
127
|
+
}
|
|
128
|
+
return emitter.on(event, onceWrapper)
|
|
129
|
+
}),
|
|
130
|
+
|
|
131
|
+
emit: jest.fn((event, ...args) => {
|
|
132
|
+
const eventListeners = listeners.get(event)
|
|
133
|
+
if (eventListeners) {
|
|
134
|
+
eventListeners.forEach(listener => listener(...args))
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
return false
|
|
138
|
+
}),
|
|
139
|
+
|
|
140
|
+
removeListener: jest.fn((event, listener) => {
|
|
141
|
+
const eventListeners = listeners.get(event)
|
|
142
|
+
if (eventListeners) {
|
|
143
|
+
const index = eventListeners.indexOf(listener)
|
|
144
|
+
if (index > -1) {
|
|
145
|
+
eventListeners.splice(index, 1)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return emitter
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
removeAllListeners: jest.fn(event => {
|
|
152
|
+
if (event) {
|
|
153
|
+
listeners.delete(event)
|
|
154
|
+
} else {
|
|
155
|
+
listeners.clear()
|
|
156
|
+
}
|
|
157
|
+
return emitter
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
listenerCount: jest.fn(event => {
|
|
161
|
+
const eventListeners = listeners.get(event)
|
|
162
|
+
return eventListeners ? eventListeners.length : 0
|
|
163
|
+
}),
|
|
164
|
+
|
|
165
|
+
// Helper methods for testing
|
|
166
|
+
getListeners: event => listeners.get(event) || [],
|
|
167
|
+
getAllListeners: () => listeners
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return emitter
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Helper to wait for async operations in tests
|
|
175
|
+
*/
|
|
176
|
+
const waitFor = (ms = 0) => {
|
|
177
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Helper to wait for a condition to be true
|
|
182
|
+
*/
|
|
183
|
+
const waitForCondition = async (condition, timeout = 1000, interval = 10) => {
|
|
184
|
+
const start = Date.now()
|
|
185
|
+
|
|
186
|
+
while (Date.now() - start < timeout) {
|
|
187
|
+
if (await condition()) {
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
await waitFor(interval)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new Error(`Condition not met within ${timeout}ms`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Helper to capture console output during tests
|
|
198
|
+
*/
|
|
199
|
+
const captureConsole = () => {
|
|
200
|
+
const originalLog = console.log
|
|
201
|
+
const originalError = console.error
|
|
202
|
+
const originalWarn = console.warn
|
|
203
|
+
const originalInfo = console.info
|
|
204
|
+
|
|
205
|
+
const logs = []
|
|
206
|
+
const errors = []
|
|
207
|
+
const warnings = []
|
|
208
|
+
const infos = []
|
|
209
|
+
|
|
210
|
+
console.log = jest.fn((...args) => {
|
|
211
|
+
logs.push(args.join(' '))
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
console.error = jest.fn((...args) => {
|
|
215
|
+
errors.push(args.join(' '))
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
console.warn = jest.fn((...args) => {
|
|
219
|
+
warnings.push(args.join(' '))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
console.info = jest.fn((...args) => {
|
|
223
|
+
infos.push(args.join(' '))
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const restore = () => {
|
|
227
|
+
console.log = originalLog
|
|
228
|
+
console.error = originalError
|
|
229
|
+
console.warn = originalWarn
|
|
230
|
+
console.info = originalInfo
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
logs,
|
|
235
|
+
errors,
|
|
236
|
+
warnings,
|
|
237
|
+
infos,
|
|
238
|
+
restore
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Helper to create a mock stream
|
|
244
|
+
*/
|
|
245
|
+
const createMockStream = (readable = true, writable = true) => {
|
|
246
|
+
const stream = createMockEventEmitter()
|
|
247
|
+
|
|
248
|
+
Object.assign(stream, {
|
|
249
|
+
readable,
|
|
250
|
+
writable,
|
|
251
|
+
destroyed: false,
|
|
252
|
+
|
|
253
|
+
read: jest.fn(),
|
|
254
|
+
write: jest.fn((chunk, encoding, callback) => {
|
|
255
|
+
if (typeof encoding === 'function') {
|
|
256
|
+
callback = encoding
|
|
257
|
+
}
|
|
258
|
+
if (callback) callback()
|
|
259
|
+
return true
|
|
260
|
+
}),
|
|
261
|
+
end: jest.fn((chunk, encoding, callback) => {
|
|
262
|
+
if (chunk) stream.write(chunk, encoding)
|
|
263
|
+
if (typeof encoding === 'function') {
|
|
264
|
+
callback = encoding
|
|
265
|
+
}
|
|
266
|
+
if (typeof chunk === 'function') {
|
|
267
|
+
callback = chunk
|
|
268
|
+
}
|
|
269
|
+
if (callback) callback()
|
|
270
|
+
stream.emit('end')
|
|
271
|
+
}),
|
|
272
|
+
destroy: jest.fn(error => {
|
|
273
|
+
stream.destroyed = true
|
|
274
|
+
if (error) {
|
|
275
|
+
stream.emit('error', error)
|
|
276
|
+
}
|
|
277
|
+
stream.emit('close')
|
|
278
|
+
}),
|
|
279
|
+
pipe: jest.fn(destination => {
|
|
280
|
+
return destination
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return stream
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Helper assertions for common test patterns
|
|
289
|
+
*/
|
|
290
|
+
const assertions = {
|
|
291
|
+
/**
|
|
292
|
+
* Assert that a mock function was called with specific arguments
|
|
293
|
+
*/
|
|
294
|
+
toHaveBeenCalledWithArgs: (mockFn, ...expectedArgs) => {
|
|
295
|
+
expect(mockFn).toHaveBeenCalledWith(...expectedArgs)
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Assert that an object has all required properties
|
|
300
|
+
*/
|
|
301
|
+
toHaveRequiredProperties: (obj, properties) => {
|
|
302
|
+
properties.forEach(prop => {
|
|
303
|
+
expect(obj).toHaveProperty(prop)
|
|
304
|
+
})
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Assert that a function throws a specific error
|
|
309
|
+
*/
|
|
310
|
+
toThrowErrorWithMessage: (fn, expectedMessage) => {
|
|
311
|
+
expect(fn).toThrow(expectedMessage)
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Assert that an async function resolves to a specific value
|
|
316
|
+
*/
|
|
317
|
+
toResolveWith: async (promise, expectedValue) => {
|
|
318
|
+
const result = await promise
|
|
319
|
+
expect(result).toEqual(expectedValue)
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Assert that an async function rejects with a specific error
|
|
324
|
+
*/
|
|
325
|
+
toRejectWith: async (promise, expectedError) => {
|
|
326
|
+
await expect(promise).rejects.toThrow(expectedError)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Helper to create a test suite with common setup/teardown
|
|
332
|
+
*/
|
|
333
|
+
const createTestSuite = (suiteName, setupFn, teardownFn) => {
|
|
334
|
+
return testFn => {
|
|
335
|
+
describe(suiteName, () => {
|
|
336
|
+
beforeEach(() => {
|
|
337
|
+
setupGlobalMocks()
|
|
338
|
+
if (setupFn) setupFn()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
afterEach(() => {
|
|
342
|
+
if (teardownFn) teardownFn()
|
|
343
|
+
cleanupGlobalMocks()
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
testFn()
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = {
|
|
352
|
+
setupGlobalMocks,
|
|
353
|
+
cleanupGlobalMocks,
|
|
354
|
+
createMockTimer,
|
|
355
|
+
mockTimers,
|
|
356
|
+
createMockEventEmitter,
|
|
357
|
+
waitFor,
|
|
358
|
+
waitForCondition,
|
|
359
|
+
captureConsole,
|
|
360
|
+
createMockStream,
|
|
361
|
+
assertions,
|
|
362
|
+
createTestSuite
|
|
363
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock implementation of tls module for server tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const {createMockEventEmitter, createMockStream} = require('./testHelpers')
|
|
6
|
+
|
|
7
|
+
const createMockSecureContext = (options = {}) => {
|
|
8
|
+
return {
|
|
9
|
+
context: 'mock-secure-context',
|
|
10
|
+
options: options,
|
|
11
|
+
// Mock methods that might be called on the context
|
|
12
|
+
setCert: jest.fn(),
|
|
13
|
+
setKey: jest.fn(),
|
|
14
|
+
addCACert: jest.fn(),
|
|
15
|
+
addCRL: jest.fn(),
|
|
16
|
+
addRootCerts: jest.fn()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const createMockTLSSocket = (socket, options = {}) => {
|
|
21
|
+
const tlsSocket = Object.assign(createMockStream(true, true), createMockEventEmitter())
|
|
22
|
+
|
|
23
|
+
Object.assign(tlsSocket, {
|
|
24
|
+
// TLS-specific properties
|
|
25
|
+
authorized: true,
|
|
26
|
+
authorizationError: null,
|
|
27
|
+
encrypted: true,
|
|
28
|
+
|
|
29
|
+
// Certificate information
|
|
30
|
+
getPeerCertificate: jest.fn((detailed = false) => {
|
|
31
|
+
return {
|
|
32
|
+
subject: {
|
|
33
|
+
CN: 'example.com',
|
|
34
|
+
O: 'Test Organization',
|
|
35
|
+
C: 'US'
|
|
36
|
+
},
|
|
37
|
+
issuer: {
|
|
38
|
+
CN: 'Test CA',
|
|
39
|
+
O: 'Test CA Organization',
|
|
40
|
+
C: 'US'
|
|
41
|
+
},
|
|
42
|
+
valid_from: 'Jan 1 00:00:00 2023 GMT',
|
|
43
|
+
valid_to: 'Jan 1 00:00:00 2024 GMT',
|
|
44
|
+
fingerprint: 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD',
|
|
45
|
+
serialNumber: '01',
|
|
46
|
+
raw: Buffer.from('mock-certificate-data')
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
getCipher: jest.fn(() => {
|
|
51
|
+
return {
|
|
52
|
+
name: 'ECDHE-RSA-AES128-GCM-SHA256',
|
|
53
|
+
version: 'TLSv1.2'
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
getProtocol: jest.fn(() => 'TLSv1.2'),
|
|
58
|
+
|
|
59
|
+
getSession: jest.fn(() => Buffer.from('mock-session-data')),
|
|
60
|
+
|
|
61
|
+
getTLSTicket: jest.fn(() => Buffer.from('mock-tls-ticket')),
|
|
62
|
+
|
|
63
|
+
renegotiate: jest.fn((options, callback) => {
|
|
64
|
+
if (typeof options === 'function') {
|
|
65
|
+
callback = options
|
|
66
|
+
options = {}
|
|
67
|
+
}
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
if (callback) callback(null)
|
|
70
|
+
}, 0)
|
|
71
|
+
return true
|
|
72
|
+
}),
|
|
73
|
+
|
|
74
|
+
setMaxSendFragment: jest.fn(size => {
|
|
75
|
+
return true
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
// Underlying socket
|
|
79
|
+
socket: socket || createMockStream(true, true)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return tlsSocket
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const createMockServer = (options = {}, secureConnectionListener) => {
|
|
86
|
+
if (typeof options === 'function') {
|
|
87
|
+
secureConnectionListener = options
|
|
88
|
+
options = {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const server = Object.assign(createMockEventEmitter(), {
|
|
92
|
+
listening: false,
|
|
93
|
+
maxConnections: null,
|
|
94
|
+
connections: 0,
|
|
95
|
+
|
|
96
|
+
listen: jest.fn((port, hostname, backlog, callback) => {
|
|
97
|
+
// Handle various argument combinations
|
|
98
|
+
if (typeof port === 'function') {
|
|
99
|
+
callback = port
|
|
100
|
+
port = 0
|
|
101
|
+
} else if (typeof hostname === 'function') {
|
|
102
|
+
callback = hostname
|
|
103
|
+
hostname = undefined
|
|
104
|
+
} else if (typeof backlog === 'function') {
|
|
105
|
+
callback = backlog
|
|
106
|
+
backlog = undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
server.listening = true
|
|
110
|
+
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
server.emit('listening')
|
|
113
|
+
if (callback) callback()
|
|
114
|
+
}, 0)
|
|
115
|
+
|
|
116
|
+
return server
|
|
117
|
+
}),
|
|
118
|
+
|
|
119
|
+
close: jest.fn(callback => {
|
|
120
|
+
server.listening = false
|
|
121
|
+
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
server.emit('close')
|
|
124
|
+
if (callback) callback()
|
|
125
|
+
}, 0)
|
|
126
|
+
|
|
127
|
+
return server
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
address: jest.fn(() => {
|
|
131
|
+
return {
|
|
132
|
+
port: 443,
|
|
133
|
+
family: 'IPv4',
|
|
134
|
+
address: '0.0.0.0'
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
getConnections: jest.fn(callback => {
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
callback(null, server.connections)
|
|
141
|
+
}, 0)
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
// Test helper methods
|
|
145
|
+
simulateConnection: socket => {
|
|
146
|
+
const tlsSocket = createMockTLSSocket(socket)
|
|
147
|
+
server.connections++
|
|
148
|
+
|
|
149
|
+
if (secureConnectionListener) {
|
|
150
|
+
secureConnectionListener(tlsSocket)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
server.emit('secureConnection', tlsSocket)
|
|
154
|
+
return tlsSocket
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
simulateError: error => {
|
|
158
|
+
server.emit('error', error)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return server
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tls = {
|
|
166
|
+
// Create secure context
|
|
167
|
+
createSecureContext: jest.fn((options = {}) => {
|
|
168
|
+
return createMockSecureContext(options)
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
// Create TLS server
|
|
172
|
+
createServer: jest.fn((options, secureConnectionListener) => {
|
|
173
|
+
return createMockServer(options, secureConnectionListener)
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
// Create TLS connection
|
|
177
|
+
connect: jest.fn((port, host, options, callback) => {
|
|
178
|
+
// Handle various argument combinations
|
|
179
|
+
if (typeof port === 'object') {
|
|
180
|
+
options = port
|
|
181
|
+
port = options.port
|
|
182
|
+
host = options.host
|
|
183
|
+
} else if (typeof host === 'object') {
|
|
184
|
+
callback = options
|
|
185
|
+
options = host
|
|
186
|
+
host = 'localhost'
|
|
187
|
+
} else if (typeof host === 'function') {
|
|
188
|
+
callback = host
|
|
189
|
+
host = 'localhost'
|
|
190
|
+
options = {}
|
|
191
|
+
} else if (typeof options === 'function') {
|
|
192
|
+
callback = options
|
|
193
|
+
options = {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const socket = createMockTLSSocket()
|
|
197
|
+
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
socket.emit('connect')
|
|
200
|
+
socket.emit('secureConnect')
|
|
201
|
+
if (callback) callback()
|
|
202
|
+
}, 0)
|
|
203
|
+
|
|
204
|
+
return socket
|
|
205
|
+
}),
|
|
206
|
+
|
|
207
|
+
// Get ciphers
|
|
208
|
+
getCiphers: jest.fn(() => {
|
|
209
|
+
return [
|
|
210
|
+
'ECDHE-RSA-AES128-GCM-SHA256',
|
|
211
|
+
'ECDHE-RSA-AES256-GCM-SHA384',
|
|
212
|
+
'ECDHE-RSA-AES128-SHA256',
|
|
213
|
+
'ECDHE-RSA-AES256-SHA384',
|
|
214
|
+
'AES128-GCM-SHA256',
|
|
215
|
+
'AES256-GCM-SHA384'
|
|
216
|
+
]
|
|
217
|
+
}),
|
|
218
|
+
|
|
219
|
+
// Constants
|
|
220
|
+
CLIENT_RENEG_LIMIT: 3,
|
|
221
|
+
CLIENT_RENEG_WINDOW: 600,
|
|
222
|
+
|
|
223
|
+
// Test helpers
|
|
224
|
+
__createMockSecureContext: createMockSecureContext,
|
|
225
|
+
__createMockTLSSocket: createMockTLSSocket,
|
|
226
|
+
__createMockServer: createMockServer
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = tls
|