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,411 @@
1
+ /**
2
+ * Unit tests for Mail.js server initialization and database setup
3
+ * Tests SMTP/IMAP server creation and database initialization
4
+ */
5
+
6
+ const {setupGlobalMocks, cleanupGlobalMocks, createMockEventEmitter} = require('./__mocks__/testHelpers')
7
+ const {createMockWebsiteConfig} = require('./__mocks__/testFactories')
8
+
9
+ // Mock external dependencies
10
+ jest.mock('smtp-server')
11
+ jest.mock('sqlite3')
12
+ jest.mock('fs')
13
+ jest.mock('os')
14
+ jest.mock('../../server/src/mail/server', () => require('./__mocks__/server/src/mail/server'))
15
+ jest.mock('tls')
16
+
17
+ const {SMTPServer} = require('smtp-server')
18
+ const sqlite3 = require('sqlite3')
19
+ const fs = require('fs')
20
+ const os = require('os')
21
+ const mailServer = require('../../server/src/mail/server')
22
+ const tls = require('tls')
23
+
24
+ describe('Mail Module - Server Initialization and Database Setup', () => {
25
+ let Mail
26
+ let mockDb
27
+ let mockConfig
28
+
29
+ beforeEach(() => {
30
+ setupGlobalMocks()
31
+
32
+ // Setup mock database
33
+ mockDb = {
34
+ serialize: jest.fn(callback => callback && callback()),
35
+ run: jest.fn((sql, params, callback) => {
36
+ if (typeof params === 'function') {
37
+ callback = params
38
+ }
39
+ if (callback) callback(null)
40
+ })
41
+ }
42
+
43
+ // Setup sqlite3 mock
44
+ sqlite3.verbose.mockReturnValue({
45
+ Database: jest.fn().mockImplementation((path, callback) => {
46
+ if (callback) callback(null)
47
+ return mockDb
48
+ })
49
+ })
50
+
51
+ // Setup mock config with MX records
52
+ mockConfig = {
53
+ config: {
54
+ websites: {
55
+ 'example.com': createMockWebsiteConfig('example.com', {
56
+ DNS: {
57
+ MX: [{name: 'example.com', value: 'mail.example.com', priority: 10}]
58
+ }
59
+ })
60
+ }
61
+ }
62
+ }
63
+
64
+ // Setup global Candy mocks
65
+ global.Candy.setMock('core', 'Config', mockConfig)
66
+ global.Candy.setMock('server', 'Log', {
67
+ init: jest.fn().mockReturnValue({
68
+ log: jest.fn(),
69
+ error: jest.fn()
70
+ })
71
+ })
72
+
73
+ // Setup os mock
74
+ os.homedir.mockReturnValue('/home/user')
75
+
76
+ // Setup fs mock
77
+ fs.existsSync.mockReturnValue(true)
78
+ fs.mkdirSync.mockImplementation(() => {})
79
+ fs.readFileSync.mockReturnValue('mock-cert-content')
80
+
81
+ // Setup SMTP server mock
82
+ const mockSMTPServer = createMockEventEmitter()
83
+ mockSMTPServer.listen = jest.fn()
84
+ SMTPServer.mockImplementation(() => mockSMTPServer)
85
+
86
+ // Setup mail server mock
87
+ const mockMailServer = createMockEventEmitter()
88
+ mockMailServer.listen = jest.fn()
89
+ mailServer.mockImplementation(() => mockMailServer)
90
+
91
+ // Setup TLS mock
92
+ tls.createSecureContext.mockReturnValue({})
93
+
94
+ // Clear module cache and require fresh instance
95
+ jest.resetModules()
96
+ Mail = require('../../server/src/Mail')
97
+ })
98
+
99
+ afterEach(() => {
100
+ cleanupGlobalMocks()
101
+ jest.clearAllMocks()
102
+ })
103
+
104
+ describe('SMTP Server Initialization', () => {
105
+ test('should initialize SMTP server on port 25', () => {
106
+ // Act
107
+ Mail.init()
108
+
109
+ // Assert
110
+ expect(SMTPServer).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ logger: true,
113
+ secure: false,
114
+ banner: 'CandyPack',
115
+ size: 1024 * 1024 * 10,
116
+ authOptional: true
117
+ })
118
+ )
119
+
120
+ const smtpInstance = SMTPServer.mock.results[0].value
121
+ expect(smtpInstance.listen).toHaveBeenCalledWith(25)
122
+ })
123
+
124
+ test('should initialize secure SMTP server on port 465', () => {
125
+ // Act
126
+ Mail.init()
127
+
128
+ // Assert
129
+ expect(SMTPServer).toHaveBeenCalledTimes(2)
130
+
131
+ const secureSmtpInstance = SMTPServer.mock.results[1].value
132
+ expect(secureSmtpInstance.listen).toHaveBeenCalledWith(465)
133
+ })
134
+
135
+ test('should configure secure SMTP server with SSL options', () => {
136
+ // Act
137
+ Mail.init()
138
+
139
+ // Assert
140
+ const secureSmtpOptions = SMTPServer.mock.calls[1][0]
141
+ expect(secureSmtpOptions.secure).toBe(true)
142
+ expect(secureSmtpOptions).toHaveProperty('SNICallback')
143
+ })
144
+ })
145
+
146
+ describe('IMAP Server Initialization', () => {
147
+ test('should initialize IMAP server on port 143', () => {
148
+ // Act
149
+ Mail.init()
150
+
151
+ // Assert
152
+ expect(mailServer).toHaveBeenCalledWith(
153
+ expect.objectContaining({
154
+ logger: true,
155
+ secure: false,
156
+ banner: 'CandyPack'
157
+ })
158
+ )
159
+
160
+ const imapInstance = mailServer.mock.results[0].value
161
+ expect(imapInstance.listen).toHaveBeenCalledWith(143)
162
+ })
163
+
164
+ test('should initialize secure IMAP server on port 993', () => {
165
+ // Act
166
+ Mail.init()
167
+
168
+ // Assert
169
+ expect(mailServer).toHaveBeenCalledTimes(2)
170
+
171
+ const secureImapInstance = mailServer.mock.results[1].value
172
+ expect(secureImapInstance.listen).toHaveBeenCalledWith(993)
173
+ })
174
+ })
175
+
176
+ describe('Database Setup', () => {
177
+ test('should create SQLite database with correct path', () => {
178
+ // Act
179
+ Mail.init()
180
+
181
+ // Assert
182
+ expect(sqlite3.verbose().Database).toHaveBeenCalledWith('/home/user/.candypack/db/mail', expect.any(Function))
183
+ })
184
+
185
+ test('should create mail database tables on initialization', () => {
186
+ // Act
187
+ Mail.init()
188
+
189
+ // Assert
190
+ expect(mockDb.run).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS mail_received'))
191
+ expect(mockDb.run).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS mail_account'))
192
+ expect(mockDb.run).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS mail_box'))
193
+ })
194
+
195
+ test('should create database indexes on initialization', () => {
196
+ // Act
197
+ Mail.init()
198
+
199
+ // Assert
200
+ expect(mockDb.run).toHaveBeenCalledWith(expect.stringContaining('CREATE INDEX IF NOT EXISTS idx_email ON mail_account'))
201
+ expect(mockDb.run).toHaveBeenCalledWith(expect.stringContaining('CREATE INDEX IF NOT EXISTS idx_domain ON mail_account'))
202
+ expect(mockDb.run).toHaveBeenCalledWith(expect.stringContaining('CREATE INDEX IF NOT EXISTS idx_uid ON mail_received'))
203
+ })
204
+
205
+ test('should create database directory if it does not exist', () => {
206
+ // Arrange
207
+ fs.existsSync.mockReturnValue(false)
208
+
209
+ // Act
210
+ Mail.init()
211
+
212
+ // Assert
213
+ expect(fs.mkdirSync).toHaveBeenCalledWith('/home/user/.candypack/db', {recursive: true})
214
+ })
215
+
216
+ test('should handle database connection errors', () => {
217
+ // Arrange
218
+ const dbError = new Error('Database connection failed')
219
+ const mockError = jest.fn()
220
+
221
+ sqlite3.verbose.mockReturnValue({
222
+ Database: jest.fn().mockImplementation((path, callback) => {
223
+ callback(dbError)
224
+ return mockDb
225
+ })
226
+ })
227
+
228
+ global.Candy.setMock('server', 'Log', {
229
+ init: jest.fn().mockReturnValue({
230
+ log: jest.fn(),
231
+ error: mockError
232
+ })
233
+ })
234
+
235
+ // Clear module cache and require fresh instance
236
+ jest.resetModules()
237
+ const FreshMail = require('../../server/src/Mail')
238
+
239
+ // Act
240
+ FreshMail.init()
241
+
242
+ // Assert
243
+ expect(mockError).toHaveBeenCalledWith(dbError.message)
244
+ })
245
+ })
246
+
247
+ describe('SSL/TLS Configuration', () => {
248
+ test('should setup SNI callback for SSL certificate selection', () => {
249
+ // Arrange
250
+ mockConfig.config.ssl = {
251
+ key: '/etc/ssl/private/default.key',
252
+ cert: '/etc/ssl/certs/default.crt'
253
+ }
254
+
255
+ // Act
256
+ Mail.init()
257
+
258
+ // Assert
259
+ const smtpOptions = SMTPServer.mock.calls[1][0] // Second call is for secure SMTP
260
+ expect(smtpOptions).toHaveProperty('SNICallback')
261
+ expect(tls.createSecureContext).toHaveBeenCalled()
262
+ })
263
+
264
+ test('should use website-specific SSL certificates when available', () => {
265
+ // Arrange
266
+ const mockWebsite = createMockWebsiteConfig('test.com')
267
+ mockConfig.config.websites['test.com'] = mockWebsite
268
+ mockConfig.config.ssl = {
269
+ key: '/etc/ssl/private/default.key',
270
+ cert: '/etc/ssl/certs/default.crt'
271
+ }
272
+
273
+ fs.existsSync.mockImplementation(path => {
274
+ return path.includes('test.com') // Only test.com certs exist
275
+ })
276
+
277
+ // Act
278
+ Mail.init()
279
+
280
+ // Get the SNI callback
281
+ const smtpOptions = SMTPServer.mock.calls[1][0]
282
+ const sniCallback = smtpOptions.SNICallback
283
+ const mockCallback = jest.fn()
284
+
285
+ // Test SNI callback with existing cert
286
+ sniCallback('test.com', mockCallback)
287
+
288
+ // Assert
289
+ expect(fs.existsSync).toHaveBeenCalledWith(mockWebsite.cert.ssl.key)
290
+ expect(fs.existsSync).toHaveBeenCalledWith(mockWebsite.cert.ssl.cert)
291
+ expect(fs.readFileSync).toHaveBeenCalledWith(mockWebsite.cert.ssl.key)
292
+ expect(fs.readFileSync).toHaveBeenCalledWith(mockWebsite.cert.ssl.cert)
293
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
294
+ key: 'mock-cert-content',
295
+ cert: 'mock-cert-content'
296
+ })
297
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
298
+ })
299
+
300
+ test('should fallback to default SSL certificates when website certs not found', () => {
301
+ // Arrange
302
+ mockConfig.config.ssl = {
303
+ key: '/etc/ssl/private/default.key',
304
+ cert: '/etc/ssl/certs/default.crt'
305
+ }
306
+
307
+ fs.existsSync.mockImplementation(path => {
308
+ return !path.includes('test.com') // Website certs don't exist
309
+ })
310
+
311
+ // Act
312
+ Mail.init()
313
+
314
+ // Get the SNI callback
315
+ const smtpOptions = SMTPServer.mock.calls[1][0]
316
+ const sniCallback = smtpOptions.SNICallback
317
+ const mockCallback = jest.fn()
318
+
319
+ // Test SNI callback with missing website cert
320
+ sniCallback('test.com', mockCallback)
321
+
322
+ // Assert
323
+ expect(fs.readFileSync).toHaveBeenCalledWith('/etc/ssl/private/default.key')
324
+ expect(fs.readFileSync).toHaveBeenCalledWith('/etc/ssl/certs/default.crt')
325
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
326
+ key: 'mock-cert-content',
327
+ cert: 'mock-cert-content'
328
+ })
329
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
330
+ })
331
+ })
332
+
333
+ describe('Initialization Conditions', () => {
334
+ test('should not initialize if no domains have MX records', () => {
335
+ // Arrange
336
+ mockConfig.config.websites = {
337
+ 'example.com': createMockWebsiteConfig('example.com', {
338
+ DNS: {
339
+ A: [{name: 'example.com', value: '127.0.0.1'}]
340
+ // No MX records
341
+ }
342
+ })
343
+ }
344
+
345
+ // Act
346
+ Mail.init()
347
+
348
+ // Assert
349
+ expect(SMTPServer).not.toHaveBeenCalled()
350
+ expect(mailServer).not.toHaveBeenCalled()
351
+ })
352
+
353
+ test('should not initialize twice if already started', () => {
354
+ // Act
355
+ Mail.init()
356
+ Mail.init() // Second call
357
+
358
+ // Assert
359
+ expect(SMTPServer).toHaveBeenCalledTimes(2) // Only from first init
360
+ expect(mailServer).toHaveBeenCalledTimes(2) // Only from first init
361
+ })
362
+ })
363
+
364
+ describe('Error Handling', () => {
365
+ test('should handle SMTP server errors', () => {
366
+ // Arrange
367
+ const mockLog = jest.fn()
368
+ global.Candy.setMock('server', 'Log', {
369
+ init: jest.fn().mockReturnValue({
370
+ log: mockLog,
371
+ error: jest.fn()
372
+ })
373
+ })
374
+
375
+ // Act
376
+ Mail.init()
377
+
378
+ // Simulate SMTP server error
379
+ const smtpInstance = SMTPServer.mock.results[0].value
380
+ const errorHandler = smtpInstance.on.mock.calls.find(call => call[0] === 'error')[1]
381
+ const testError = new Error('SMTP Server Error')
382
+ errorHandler(testError)
383
+
384
+ // Assert
385
+ expect(mockLog).toHaveBeenCalledWith('SMTP Server Error: ', testError)
386
+ })
387
+
388
+ test('should handle secure SMTP server errors', () => {
389
+ // Arrange
390
+ const mockError = jest.fn()
391
+ global.Candy.setMock('server', 'Log', {
392
+ init: jest.fn().mockReturnValue({
393
+ log: jest.fn(),
394
+ error: mockError
395
+ })
396
+ })
397
+
398
+ // Act
399
+ Mail.init()
400
+
401
+ // Simulate secure SMTP server error
402
+ const secureSmtpInstance = SMTPServer.mock.results[1].value
403
+ const errorHandler = secureSmtpInstance.on.mock.calls.find(call => call[0] === 'error')[1]
404
+ const testError = new Error('Secure SMTP Server Error')
405
+ errorHandler(testError)
406
+
407
+ // Assert
408
+ expect(mockError).toHaveBeenCalledWith('SMTP Server Error: ', testError)
409
+ })
410
+ })
411
+ })