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.
Files changed (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. package/web/view/header/main.html +14 -0
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Mock implementation of the path module for server tests
3
+ * Provides comprehensive mocking of path manipulation utilities
4
+ */
5
+
6
+ const path = {
7
+ // Path separator
8
+ sep: '/',
9
+ delimiter: ':',
10
+
11
+ // POSIX and Windows path objects
12
+ posix: null, // Will be set to this object
13
+ win32: null, // Will be set to a Windows version
14
+
15
+ // Path manipulation functions
16
+ basename: jest.fn((path, ext) => {
17
+ if (typeof path !== 'string') {
18
+ throw new TypeError('Path must be a string')
19
+ }
20
+
21
+ const lastSlash = path.lastIndexOf('/')
22
+ let base = lastSlash === -1 ? path : path.slice(lastSlash + 1)
23
+
24
+ if (ext && base.endsWith(ext)) {
25
+ base = base.slice(0, -ext.length)
26
+ }
27
+
28
+ return base
29
+ }),
30
+
31
+ dirname: jest.fn(path => {
32
+ if (typeof path !== 'string') {
33
+ throw new TypeError('Path must be a string')
34
+ }
35
+
36
+ if (path === '/') return '/'
37
+ if (path === '') return '.'
38
+
39
+ const lastSlash = path.lastIndexOf('/')
40
+ if (lastSlash === -1) return '.'
41
+ if (lastSlash === 0) return '/'
42
+
43
+ return path.slice(0, lastSlash)
44
+ }),
45
+
46
+ extname: jest.fn(path => {
47
+ if (typeof path !== 'string') {
48
+ throw new TypeError('Path must be a string')
49
+ }
50
+
51
+ const lastDot = path.lastIndexOf('.')
52
+ const lastSlash = path.lastIndexOf('/')
53
+
54
+ if (lastDot === -1 || lastDot < lastSlash) {
55
+ return ''
56
+ }
57
+
58
+ return path.slice(lastDot)
59
+ }),
60
+
61
+ format: jest.fn(pathObject => {
62
+ if (typeof pathObject !== 'object' || pathObject === null) {
63
+ throw new TypeError('Path object must be an object')
64
+ }
65
+
66
+ const {dir, root, base, name, ext} = pathObject
67
+
68
+ if (base) {
69
+ return dir ? `${dir}/${base}` : base
70
+ }
71
+
72
+ const filename = name + (ext || '')
73
+
74
+ if (dir) {
75
+ return `${dir}/${filename}`
76
+ } else if (root) {
77
+ return `${root}${filename}`
78
+ }
79
+
80
+ return filename
81
+ }),
82
+
83
+ isAbsolute: jest.fn(path => {
84
+ if (typeof path !== 'string') {
85
+ throw new TypeError('Path must be a string')
86
+ }
87
+
88
+ return path.startsWith('/')
89
+ }),
90
+
91
+ join: jest.fn((...paths) => {
92
+ if (paths.length === 0) return '.'
93
+
94
+ const filteredPaths = paths.filter(p => typeof p === 'string' && p !== '')
95
+ if (filteredPaths.length === 0) return '.'
96
+
97
+ let joined = filteredPaths.join('/')
98
+
99
+ // Normalize the path
100
+ return path.normalize(joined)
101
+ }),
102
+
103
+ normalize: jest.fn(path => {
104
+ if (typeof path !== 'string') {
105
+ throw new TypeError('Path must be a string')
106
+ }
107
+
108
+ if (path === '') return '.'
109
+
110
+ const isAbsolute = path.startsWith('/')
111
+ const trailingSlash = path.endsWith('/')
112
+
113
+ // Split path into segments and process
114
+ const segments = path.split('/').filter(segment => segment !== '' && segment !== '.')
115
+ const normalizedSegments = []
116
+
117
+ for (const segment of segments) {
118
+ if (segment === '..') {
119
+ if (normalizedSegments.length > 0 && normalizedSegments[normalizedSegments.length - 1] !== '..') {
120
+ normalizedSegments.pop()
121
+ } else if (!isAbsolute) {
122
+ normalizedSegments.push('..')
123
+ }
124
+ } else {
125
+ normalizedSegments.push(segment)
126
+ }
127
+ }
128
+
129
+ let result = normalizedSegments.join('/')
130
+
131
+ if (isAbsolute) {
132
+ result = '/' + result
133
+ } else if (result === '') {
134
+ result = '.'
135
+ }
136
+
137
+ if (trailingSlash && result !== '/' && result !== '.') {
138
+ result += '/'
139
+ }
140
+
141
+ return result
142
+ }),
143
+
144
+ parse: jest.fn(path => {
145
+ if (typeof path !== 'string') {
146
+ throw new TypeError('Path must be a string')
147
+ }
148
+
149
+ const isAbsolute = path.startsWith('/')
150
+ const lastSlash = path.lastIndexOf('/')
151
+ const lastDot = path.lastIndexOf('.')
152
+
153
+ let dir = ''
154
+ let base = ''
155
+ let ext = ''
156
+ let name = ''
157
+ let root = ''
158
+
159
+ if (isAbsolute) {
160
+ root = '/'
161
+ }
162
+
163
+ if (lastSlash === -1) {
164
+ base = path
165
+ dir = isAbsolute ? '/' : ''
166
+ } else {
167
+ dir = path.slice(0, lastSlash) || (isAbsolute ? '/' : '')
168
+ base = path.slice(lastSlash + 1)
169
+ }
170
+
171
+ if (lastDot > lastSlash && lastDot > 0) {
172
+ ext = path.slice(lastDot)
173
+ name = base.slice(0, base.length - ext.length)
174
+ } else {
175
+ name = base
176
+ }
177
+
178
+ return {root, dir, base, ext, name}
179
+ }),
180
+
181
+ relative: jest.fn((from, to) => {
182
+ if (typeof from !== 'string' || typeof to !== 'string') {
183
+ throw new TypeError('From and to must be strings')
184
+ }
185
+
186
+ if (from === to) return ''
187
+
188
+ const fromParts = path
189
+ .resolve(from)
190
+ .split('/')
191
+ .filter(p => p !== '')
192
+ const toParts = path
193
+ .resolve(to)
194
+ .split('/')
195
+ .filter(p => p !== '')
196
+
197
+ // Find common prefix
198
+ let commonLength = 0
199
+ const minLength = Math.min(fromParts.length, toParts.length)
200
+
201
+ for (let i = 0; i < minLength; i++) {
202
+ if (fromParts[i] === toParts[i]) {
203
+ commonLength++
204
+ } else {
205
+ break
206
+ }
207
+ }
208
+
209
+ // Build relative path
210
+ const upCount = fromParts.length - commonLength
211
+ const relativeParts = Array(upCount).fill('..').concat(toParts.slice(commonLength))
212
+
213
+ return relativeParts.join('/') || '.'
214
+ }),
215
+
216
+ resolve: jest.fn((...paths) => {
217
+ let resolvedPath = ''
218
+ let resolvedAbsolute = false
219
+
220
+ for (let i = paths.length - 1; i >= 0 && !resolvedAbsolute; i--) {
221
+ const path = paths[i]
222
+
223
+ if (typeof path !== 'string') {
224
+ throw new TypeError('Path must be a string')
225
+ }
226
+
227
+ if (path === '') continue
228
+
229
+ resolvedPath = path + '/' + resolvedPath
230
+ resolvedAbsolute = path.startsWith('/')
231
+ }
232
+
233
+ if (!resolvedAbsolute) {
234
+ resolvedPath = '/mock/cwd/' + resolvedPath
235
+ }
236
+
237
+ // Normalize and remove trailing slash
238
+ resolvedPath = path.normalize(resolvedPath)
239
+ if (resolvedPath.endsWith('/') && resolvedPath !== '/') {
240
+ resolvedPath = resolvedPath.slice(0, -1)
241
+ }
242
+
243
+ return resolvedPath
244
+ }),
245
+
246
+ // Test helpers
247
+ __setPosix: () => {
248
+ // Current implementation is already POSIX-like
249
+ return path
250
+ },
251
+
252
+ __setWin32: () => {
253
+ // Create a Windows version of path methods
254
+ const win32Path = {...path}
255
+
256
+ win32Path.sep = '\\'
257
+ win32Path.delimiter = ';'
258
+
259
+ win32Path.isAbsolute = jest.fn(path => {
260
+ if (typeof path !== 'string') {
261
+ throw new TypeError('Path must be a string')
262
+ }
263
+
264
+ return /^[a-zA-Z]:\\/.test(path) || path.startsWith('\\\\')
265
+ })
266
+
267
+ win32Path.normalize = jest.fn(path => {
268
+ if (typeof path !== 'string') {
269
+ throw new TypeError('Path must be a string')
270
+ }
271
+
272
+ return path.replace(/\//g, '\\')
273
+ })
274
+
275
+ return win32Path
276
+ },
277
+
278
+ __resetMocks: () => {
279
+ Object.values(path).forEach(fn => {
280
+ if (jest.isMockFunction(fn)) {
281
+ fn.mockClear()
282
+ }
283
+ })
284
+ }
285
+ }
286
+
287
+ // Set up posix and win32 references
288
+ path.posix = path
289
+ path.win32 = path.__setWin32()
290
+
291
+ module.exports = path
@@ -0,0 +1,8 @@
1
+ const mockSelfsigned = {
2
+ generate: jest.fn().mockReturnValue({
3
+ private: '-----BEGIN PRIVATE KEY-----\nmock-private-key\n-----END PRIVATE KEY-----',
4
+ cert: '-----BEGIN CERTIFICATE-----\nmock-certificate\n-----END CERTIFICATE-----'
5
+ })
6
+ }
7
+
8
+ module.exports = mockSelfsigned
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Mock implementation of mail/server for server tests
3
+ */
4
+
5
+ const {createMockEventEmitter} = require('../../../testHelpers')
6
+
7
+ class MockMailServer {
8
+ constructor(options = {}) {
9
+ Object.assign(this, createMockEventEmitter())
10
+
11
+ this.options = options
12
+ this.listening = false
13
+ this.port = null
14
+
15
+ // Store callbacks for testing
16
+ this.onAuth = options.onAuth
17
+ this.onAppend = options.onAppend
18
+ this.onExpunge = options.onExpunge
19
+ this.onCreate = options.onCreate
20
+ this.onDelete = options.onDelete
21
+ this.onRename = options.onRename
22
+ this.onFetch = options.onFetch
23
+ this.onList = options.onList
24
+ this.onLsub = options.onLsub
25
+ this.onSelect = options.onSelect
26
+ this.onStore = options.onStore
27
+ this.onError = options.onError
28
+ }
29
+
30
+ listen(port, hostname, callback) {
31
+ if (typeof hostname === 'function') {
32
+ callback = hostname
33
+ hostname = undefined
34
+ }
35
+
36
+ this.port = port
37
+ this.listening = true
38
+
39
+ setTimeout(() => {
40
+ this.emit('listening')
41
+ if (callback) callback()
42
+ }, 0)
43
+
44
+ return this
45
+ }
46
+
47
+ close(callback) {
48
+ this.listening = false
49
+ this.port = null
50
+
51
+ setTimeout(() => {
52
+ this.emit('close')
53
+ if (callback) callback()
54
+ }, 0)
55
+
56
+ return this
57
+ }
58
+
59
+ // Test helper methods
60
+ simulateConnection(session = {}) {
61
+ const mockSession = {
62
+ remoteAddress: '127.0.0.1',
63
+ remotePort: 12345,
64
+ user: null,
65
+ ...session
66
+ }
67
+
68
+ this.emit('connect', mockSession)
69
+ return mockSession
70
+ }
71
+
72
+ simulateAuth(auth, session, callback) {
73
+ if (this.onAuth) {
74
+ this.onAuth(auth, session, callback)
75
+ } else if (callback) {
76
+ callback(null, {user: auth.username})
77
+ }
78
+ }
79
+
80
+ simulateAppend(data, callback) {
81
+ if (this.onAppend) {
82
+ this.onAppend(data, callback)
83
+ } else if (callback) {
84
+ callback()
85
+ }
86
+ }
87
+
88
+ simulateError(error) {
89
+ if (this.onError) {
90
+ this.onError(error)
91
+ }
92
+ this.emit('error', error)
93
+ }
94
+ }
95
+
96
+ const mockMailServer = jest.fn().mockImplementation(options => {
97
+ return new MockMailServer(options)
98
+ })
99
+
100
+ module.exports = mockMailServer
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Mock implementation of mail/smtp for server tests
3
+ */
4
+
5
+ const smtp = {
6
+ send: jest.fn(mailData => {
7
+ // Mock successful send
8
+ return Promise.resolve({
9
+ messageId: mailData.messageId || '<mock@example.com>',
10
+ accepted: mailData.to ? [mailData.to.value[0].address] : ['recipient@example.com'],
11
+ rejected: [],
12
+ pending: [],
13
+ response: '250 Message accepted'
14
+ })
15
+ }),
16
+
17
+ // Test helper methods
18
+ __setMockResponse: response => {
19
+ smtp.send.mockResolvedValue(response)
20
+ },
21
+
22
+ __setMockError: error => {
23
+ smtp.send.mockRejectedValue(error)
24
+ },
25
+
26
+ __resetMock: () => {
27
+ smtp.send.mockClear()
28
+ }
29
+ }
30
+
31
+ module.exports = smtp
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Mock implementation of smtp-server for server tests
3
+ */
4
+
5
+ const {createMockEventEmitter} = require('./testHelpers')
6
+
7
+ class MockSMTPServer {
8
+ constructor(options = {}) {
9
+ Object.assign(this, createMockEventEmitter())
10
+
11
+ this.options = options
12
+ this.listening = false
13
+ this.port = null
14
+
15
+ // Store callbacks for testing
16
+ this.onAuth = options.onAuth
17
+ this.onData = options.onData
18
+ this.onAppend = options.onAppend
19
+ this.onExpunge = options.onExpunge
20
+ this.onCreate = options.onCreate
21
+ this.onDelete = options.onDelete
22
+ this.onRename = options.onRename
23
+ this.onFetch = options.onFetch
24
+ this.onList = options.onList
25
+ this.onLsub = options.onLsub
26
+ this.onMailFrom = options.onMailFrom
27
+ this.onRcptTo = options.onRcptTo
28
+ this.onSelect = options.onSelect
29
+ this.onStore = options.onStore
30
+ this.onError = options.onError
31
+ }
32
+
33
+ listen(port, hostname, callback) {
34
+ if (typeof hostname === 'function') {
35
+ callback = hostname
36
+ hostname = undefined
37
+ }
38
+
39
+ this.port = port
40
+ this.listening = true
41
+
42
+ setTimeout(() => {
43
+ this.emit('listening')
44
+ if (callback) callback()
45
+ }, 0)
46
+
47
+ return this
48
+ }
49
+
50
+ close(callback) {
51
+ this.listening = false
52
+ this.port = null
53
+
54
+ setTimeout(() => {
55
+ this.emit('close')
56
+ if (callback) callback()
57
+ }, 0)
58
+
59
+ return this
60
+ }
61
+
62
+ // Test helper methods
63
+ simulateConnection(session = {}) {
64
+ const mockSession = {
65
+ remoteAddress: '127.0.0.1',
66
+ remotePort: 12345,
67
+ user: null,
68
+ ...session
69
+ }
70
+
71
+ this.emit('connect', mockSession)
72
+ return mockSession
73
+ }
74
+
75
+ simulateAuth(auth, session, callback) {
76
+ if (this.onAuth) {
77
+ this.onAuth(auth, session, callback)
78
+ } else if (callback) {
79
+ callback(null, {user: auth.username})
80
+ }
81
+ }
82
+
83
+ simulateData(stream, session, callback) {
84
+ if (this.onData) {
85
+ this.onData(stream, session, callback)
86
+ } else if (callback) {
87
+ callback()
88
+ }
89
+ }
90
+
91
+ simulateError(error) {
92
+ if (this.onError) {
93
+ this.onError(error)
94
+ }
95
+ this.emit('error', error)
96
+ }
97
+ }
98
+
99
+ const SMTPServer = jest.fn().mockImplementation(options => {
100
+ return new MockSMTPServer(options)
101
+ })
102
+
103
+ module.exports = {
104
+ SMTPServer,
105
+ MockSMTPServer
106
+ }