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,1562 @@
1
+ /**
2
+ * Unit tests for Web.js module
3
+ * Tests web hosting, proxy functionality, and website management
4
+ */
5
+
6
+ // Mock all required modules before importing Web
7
+ jest.mock('child_process')
8
+ jest.mock('fs')
9
+ jest.mock('http')
10
+ jest.mock('https')
11
+ jest.mock('http-proxy')
12
+ jest.mock('net')
13
+ jest.mock('os')
14
+ jest.mock('path')
15
+ jest.mock('tls')
16
+
17
+ const childProcess = require('child_process')
18
+ const fs = require('fs')
19
+ const http = require('http')
20
+ const https = require('https')
21
+ const httpProxy = require('http-proxy')
22
+ const net = require('net')
23
+ const os = require('os')
24
+ const path = require('path')
25
+ const tls = require('tls')
26
+
27
+ // Import test utilities
28
+ const {mockCandy, mockLangGet} = require('./__mocks__/globalCandy')
29
+ const {createMockRequest, createMockResponse} = require('./__mocks__/testFactories')
30
+ const {createMockWebsiteConfig} = require('./__mocks__/testFactories')
31
+
32
+ describe('Web', () => {
33
+ let Web
34
+ let mockConfig
35
+ let mockLog
36
+ let mockHttpServer
37
+ let mockHttpsServer
38
+ let mockProxyServer
39
+
40
+ beforeEach(() => {
41
+ // Reset all mocks
42
+ jest.clearAllMocks()
43
+
44
+ // Setup global Candy mock
45
+ mockCandy.resetMocks()
46
+ mockConfig = mockCandy.core('Config')
47
+
48
+ // Initialize config structure
49
+ mockConfig.config = {
50
+ websites: {},
51
+ web: {path: '/var/candypack'},
52
+ ssl: null
53
+ }
54
+
55
+ // Setup Log mock
56
+ const mockLogInstance = {
57
+ log: jest.fn(),
58
+ error: jest.fn(),
59
+ info: jest.fn(),
60
+ debug: jest.fn()
61
+ }
62
+ mockCandy.setMock('server', 'Log', {
63
+ init: jest.fn().mockReturnValue(mockLogInstance)
64
+ })
65
+ mockLog = mockLogInstance.log
66
+
67
+ // Setup Api mock
68
+ mockCandy.setMock('server', 'Api', {
69
+ result: jest.fn((success, message) => ({success, message}))
70
+ })
71
+
72
+ // Setup DNS mock with default methods
73
+ mockCandy.setMock('server', 'DNS', {
74
+ record: jest.fn(),
75
+ ip: '127.0.0.1'
76
+ })
77
+
78
+ // Setup Process mock
79
+ mockCandy.setMock('core', 'Process', {
80
+ stop: jest.fn()
81
+ })
82
+
83
+ global.Candy = mockCandy
84
+ global.__ = jest.fn((key, ...args) => {
85
+ // Simple mock translation function
86
+ let result = key
87
+ args.forEach((arg, index) => {
88
+ result = result.replace(`%s${index + 1}`, arg).replace('%s', arg)
89
+ })
90
+ return result
91
+ })
92
+
93
+ // Setup mock servers
94
+ mockHttpServer = {
95
+ listen: jest.fn(),
96
+ on: jest.fn(),
97
+ close: jest.fn()
98
+ }
99
+
100
+ mockHttpsServer = {
101
+ listen: jest.fn(),
102
+ on: jest.fn(),
103
+ close: jest.fn()
104
+ }
105
+
106
+ mockProxyServer = {
107
+ web: jest.fn(),
108
+ on: jest.fn()
109
+ }
110
+
111
+ // Setup module mocks
112
+ http.createServer.mockReturnValue(mockHttpServer)
113
+ https.createServer.mockReturnValue(mockHttpsServer)
114
+ httpProxy.createProxyServer.mockReturnValue(mockProxyServer)
115
+
116
+ // Setup file system mocks
117
+ fs.existsSync.mockReturnValue(true)
118
+ fs.mkdirSync.mockImplementation(() => {})
119
+ fs.cpSync.mockImplementation(() => {})
120
+ fs.rmSync.mockImplementation(() => {})
121
+ fs.readFileSync.mockReturnValue('mock-file-content')
122
+ fs.writeFile.mockImplementation((path, data, callback) => {
123
+ if (callback) callback(null)
124
+ })
125
+
126
+ // Setup OS mocks
127
+ os.homedir.mockReturnValue('/home/user')
128
+ os.platform.mockReturnValue('linux')
129
+
130
+ // Setup path mocks
131
+ path.join.mockImplementation((...args) => args.join('/'))
132
+
133
+ // Setup child process mocks
134
+ const mockChild = {
135
+ pid: 12345,
136
+ stdout: {on: jest.fn()},
137
+ stderr: {on: jest.fn()},
138
+ on: jest.fn()
139
+ }
140
+ childProcess.spawn.mockReturnValue(mockChild)
141
+ childProcess.execSync.mockImplementation(() => {})
142
+
143
+ // Setup net mocks for port checking
144
+ const mockNetServer = {
145
+ once: jest.fn(),
146
+ listen: jest.fn(),
147
+ close: jest.fn()
148
+ }
149
+ net.createServer.mockReturnValue(mockNetServer)
150
+
151
+ // Setup TLS mocks
152
+ const mockSecureContext = {context: 'mock-context'}
153
+ tls.createSecureContext.mockReturnValue(mockSecureContext)
154
+
155
+ // Import Web after mocks are set up
156
+ Web = require('../../server/src/Web')
157
+ })
158
+
159
+ afterEach(() => {
160
+ delete global.Candy
161
+ delete global.__
162
+ })
163
+
164
+ describe('initialization', () => {
165
+ test('should initialize with default configuration', async () => {
166
+ await Web.init()
167
+
168
+ expect(Web.server).toBeDefined()
169
+ expect(Web.server).toBeDefined()
170
+ })
171
+
172
+ test('should set default web path based on platform', async () => {
173
+ // Test Linux/Unix platform
174
+ os.platform.mockReturnValue('linux')
175
+ mockConfig.config.web = undefined
176
+
177
+ await Web.init()
178
+
179
+ expect(mockConfig.config.web.path).toBe('/var/candypack/')
180
+
181
+ // Test macOS platform
182
+ os.platform.mockReturnValue('darwin')
183
+ mockConfig.config.web = undefined
184
+
185
+ await Web.init()
186
+
187
+ expect(mockConfig.config.web.path).toBe('/home/user/Candypack/')
188
+
189
+ // Test Windows platform
190
+ os.platform.mockReturnValue('win32')
191
+ mockConfig.config.web = undefined
192
+
193
+ await Web.init()
194
+
195
+ expect(mockConfig.config.web.path).toBe('/home/user/Candypack/')
196
+ })
197
+
198
+ test('should create web directory if it does not exist', async () => {
199
+ fs.existsSync.mockReturnValue(false)
200
+ mockConfig.config.web = {path: '/custom/path'}
201
+
202
+ await Web.init()
203
+
204
+ expect(fs.existsSync).toHaveBeenCalledWith('/custom/path')
205
+ })
206
+ })
207
+
208
+ describe('server creation', () => {
209
+ beforeEach(async () => {
210
+ await Web.init()
211
+ mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
212
+ })
213
+
214
+ test('should create HTTP server on port 80', () => {
215
+ Web.server()
216
+
217
+ expect(http.createServer).toHaveBeenCalledWith(expect.any(Function))
218
+ expect(mockHttpServer.listen).toHaveBeenCalledWith(80)
219
+ expect(mockHttpServer.on).toHaveBeenCalledWith('error', expect.any(Function))
220
+ })
221
+
222
+ test('should handle HTTP server errors', () => {
223
+ // Create a fresh mock server for this test
224
+ const freshMockHttpServer = {
225
+ listen: jest.fn(),
226
+ on: jest.fn(),
227
+ close: jest.fn()
228
+ }
229
+ http.createServer.mockReturnValue(freshMockHttpServer)
230
+
231
+ // Reset the Web module's server instances to force recreation
232
+ Web['_Web__server_http'] = null
233
+ Web['_Web__server_https'] = null
234
+ Web['_Web__loaded'] = true // Ensure Web module is marked as loaded
235
+
236
+ // Ensure we have websites configured (required for server creation)
237
+ mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
238
+
239
+ Web.server()
240
+
241
+ // Verify HTTP server was created
242
+ expect(http.createServer).toHaveBeenCalled()
243
+
244
+ // Verify the error handler was attached
245
+ expect(freshMockHttpServer.on).toHaveBeenCalledWith('error', expect.any(Function))
246
+
247
+ // Get the error handler function
248
+ const errorCall = freshMockHttpServer.on.mock.calls.find(call => call[0] === 'error')
249
+ const errorHandler = errorCall[1]
250
+
251
+ const mockError = new Error('EADDRINUSE')
252
+ mockError.code = 'EADDRINUSE'
253
+
254
+ expect(() => errorHandler(mockError)).not.toThrow()
255
+ expect(mockLog).toHaveBeenCalledWith('HTTP server error: EADDRINUSE')
256
+ expect(mockLog).toHaveBeenCalledWith('Port 80 is already in use')
257
+ })
258
+
259
+ test('should create HTTPS server on port 443 with SSL configuration', () => {
260
+ mockConfig.config.ssl = {
261
+ key: '/path/to/key.pem',
262
+ cert: '/path/to/cert.pem'
263
+ }
264
+
265
+ Web.server()
266
+
267
+ expect(https.createServer).toHaveBeenCalledWith(
268
+ expect.objectContaining({
269
+ SNICallback: expect.any(Function),
270
+ key: 'mock-file-content',
271
+ cert: 'mock-file-content'
272
+ }),
273
+ expect.any(Function)
274
+ )
275
+ expect(mockHttpsServer.listen).toHaveBeenCalledWith(443)
276
+ expect(mockHttpsServer.on).toHaveBeenCalledWith('error', expect.any(Function))
277
+ })
278
+
279
+ test('should handle HTTPS server errors', () => {
280
+ mockConfig.config.ssl = {
281
+ key: '/path/to/key.pem',
282
+ cert: '/path/to/cert.pem'
283
+ }
284
+
285
+ // Create a fresh mock server for this test
286
+ const freshMockHttpsServer = {
287
+ listen: jest.fn(),
288
+ on: jest.fn(),
289
+ close: jest.fn()
290
+ }
291
+ https.createServer.mockReturnValue(freshMockHttpsServer)
292
+
293
+ // Reset the Web module's server instances to force recreation
294
+ Web['_Web__server_http'] = null
295
+ Web['_Web__server_https'] = null
296
+ Web['_Web__loaded'] = true // Ensure Web module is marked as loaded
297
+
298
+ // Ensure we have websites configured (required for server creation)
299
+ mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
300
+
301
+ Web.server()
302
+
303
+ // Verify HTTPS server was created
304
+ expect(https.createServer).toHaveBeenCalled()
305
+
306
+ // Verify the error handler was attached
307
+ expect(freshMockHttpsServer.on).toHaveBeenCalledWith('error', expect.any(Function))
308
+
309
+ // Get the error handler function
310
+ const errorCall = freshMockHttpsServer.on.mock.calls.find(call => call[0] === 'error')
311
+ const errorHandler = errorCall[1]
312
+
313
+ const mockError = new Error('EADDRINUSE')
314
+ mockError.code = 'EADDRINUSE'
315
+
316
+ expect(() => errorHandler(mockError)).not.toThrow()
317
+ expect(mockLog).toHaveBeenCalledWith('HTTPS server error: EADDRINUSE')
318
+ expect(mockLog).toHaveBeenCalledWith('Port 443 is already in use')
319
+ })
320
+
321
+ test('should not create HTTPS server without SSL configuration', () => {
322
+ mockConfig.config.ssl = undefined
323
+
324
+ Web.server()
325
+
326
+ expect(https.createServer).not.toHaveBeenCalled()
327
+ })
328
+
329
+ test('should not create HTTPS server with missing SSL files', () => {
330
+ mockConfig.config.ssl = {
331
+ key: '/path/to/key.pem',
332
+ cert: '/path/to/cert.pem'
333
+ }
334
+ fs.existsSync.mockImplementation(path => !path.includes('key.pem') && !path.includes('cert.pem'))
335
+
336
+ Web.server()
337
+
338
+ expect(https.createServer).not.toHaveBeenCalled()
339
+ })
340
+ })
341
+
342
+ describe('website creation', () => {
343
+ beforeEach(async () => {
344
+ await Web.init()
345
+ mockConfig.config.web = {path: '/var/candypack'}
346
+ })
347
+
348
+ test('should create website with valid domain', () => {
349
+ const mockProgress = jest.fn()
350
+ const domain = 'example.com'
351
+
352
+ const result = Web.create(domain, mockProgress)
353
+
354
+ expect(result.success).toBe(true)
355
+ expect(result.message).toContain('Website example.com created')
356
+ expect(mockProgress).toHaveBeenCalledWith('domain', 'progress', expect.stringContaining('Setting up domain'))
357
+ expect(mockProgress).toHaveBeenCalledWith('domain', 'success', expect.stringContaining('Domain example.com set'))
358
+ })
359
+
360
+ test('should reject invalid domain names', () => {
361
+ const mockProgress = jest.fn()
362
+
363
+ // Test short domain
364
+ let result = Web.create('ab', mockProgress)
365
+ expect(result.success).toBe(false)
366
+ expect(result.message).toBe('Invalid domain.')
367
+
368
+ // Test domain without dot (except localhost)
369
+ result = Web.create('invalid', mockProgress)
370
+ expect(result.success).toBe(false)
371
+ expect(result.message).toBe('Invalid domain.')
372
+ })
373
+
374
+ test('should allow localhost as valid domain', () => {
375
+ const mockProgress = jest.fn()
376
+
377
+ const result = Web.create('localhost', mockProgress)
378
+
379
+ expect(result.success).toBe(true)
380
+ expect(result.message).toContain('Website localhost created')
381
+ })
382
+
383
+ test('should strip protocol prefixes from domain', () => {
384
+ const mockProgress = jest.fn()
385
+
386
+ Web.create('https://example.com', mockProgress)
387
+
388
+ expect(mockConfig.config.websites['example.com']).toBeDefined()
389
+ expect(mockConfig.config.websites['https://example.com']).toBeUndefined()
390
+ })
391
+
392
+ test('should reject existing domain', () => {
393
+ const mockProgress = jest.fn()
394
+ mockConfig.config.websites = {'example.com': {}}
395
+
396
+ const result = Web.create('example.com', mockProgress)
397
+
398
+ expect(result.success).toBe(false)
399
+ expect(result.message).toBe('Website example.com already exists.')
400
+ })
401
+
402
+ test('should create website directory structure', () => {
403
+ const mockProgress = jest.fn()
404
+ const domain = 'example.com'
405
+
406
+ // Mock fs.existsSync to return false for the website directory so it gets created
407
+ fs.existsSync.mockImplementation(path => {
408
+ if (path === '/var/candypack/example.com') return false
409
+ if (path.includes('node_modules')) return false
410
+ return true
411
+ })
412
+
413
+ Web.create(domain, mockProgress)
414
+
415
+ expect(fs.mkdirSync).toHaveBeenCalledWith('/var/candypack/example.com', {recursive: true})
416
+ expect(fs.cpSync).toHaveBeenCalledWith(expect.stringContaining('web/'), '/var/candypack/example.com', {recursive: true})
417
+ })
418
+
419
+ test('should setup npm link for candypack', () => {
420
+ const mockProgress = jest.fn()
421
+ const domain = 'example.com'
422
+
423
+ Web.create(domain, mockProgress)
424
+
425
+ expect(childProcess.execSync).toHaveBeenCalledWith('npm link candypack', {
426
+ cwd: '/var/candypack/example.com'
427
+ })
428
+ })
429
+
430
+ test('should remove node_modules/.bin if it exists', () => {
431
+ const mockProgress = jest.fn()
432
+ const domain = 'example.com'
433
+ fs.existsSync.mockImplementation(path => path.includes('node_modules/.bin'))
434
+
435
+ Web.create(domain, mockProgress)
436
+
437
+ // Note: The actual Web.js code has a bug - missing '/' in path concatenation
438
+ expect(fs.rmSync).toHaveBeenCalledWith('/var/candypack/example.com/node_modules/.bin', {recursive: true})
439
+ })
440
+
441
+ test('should create node_modules directory if it does not exist', () => {
442
+ const mockProgress = jest.fn()
443
+ const domain = 'example.com'
444
+ fs.existsSync.mockImplementation(path => !path.includes('node_modules'))
445
+
446
+ Web.create(domain, mockProgress)
447
+
448
+ expect(fs.mkdirSync).toHaveBeenCalledWith('/var/candypack/example.com/node_modules')
449
+ })
450
+
451
+ test('should setup DNS records for non-localhost domains', () => {
452
+ const mockProgress = jest.fn()
453
+ const domain = 'example.com'
454
+ const mockDNS = {
455
+ record: jest.fn(),
456
+ ip: '192.168.1.1'
457
+ }
458
+ mockCandy.setMock('server', 'DNS', mockDNS)
459
+ mockCandy.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
460
+
461
+ Web.create(domain, mockProgress)
462
+
463
+ expect(mockDNS.record).toHaveBeenCalledWith(
464
+ {name: 'example.com', type: 'A', value: '192.168.1.1'},
465
+ {name: 'www.example.com', type: 'CNAME', value: 'example.com'},
466
+ {name: 'example.com', type: 'MX', value: 'example.com'},
467
+ {name: 'example.com', type: 'TXT', value: 'v=spf1 a mx ip4:192.168.1.1 ~all'},
468
+ {
469
+ name: '_dmarc.example.com',
470
+ type: 'TXT',
471
+ value: 'v=DMARC1; p=reject; rua=mailto:postmaster@example.com'
472
+ }
473
+ )
474
+ expect(mockProgress).toHaveBeenCalledWith('dns', 'progress', expect.stringContaining('Setting up DNS records'))
475
+ expect(mockProgress).toHaveBeenCalledWith('dns', 'success', expect.stringContaining('DNS records for example.com set'))
476
+ })
477
+
478
+ test('should not setup DNS records for localhost', () => {
479
+ const mockProgress = jest.fn()
480
+ const mockDNS = {record: jest.fn()}
481
+ mockCandy.setMock('server', 'DNS', mockDNS)
482
+ mockCandy.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
483
+
484
+ Web.create('localhost', mockProgress)
485
+
486
+ expect(mockDNS.record).not.toHaveBeenCalled()
487
+ })
488
+
489
+ test('should not setup DNS records for IP addresses', () => {
490
+ const mockProgress = jest.fn()
491
+ const mockDNS = {record: jest.fn()}
492
+ mockCandy.setMock('server', 'DNS', mockDNS)
493
+ mockCandy.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
494
+
495
+ Web.create('192.168.1.1', mockProgress)
496
+
497
+ expect(mockDNS.record).not.toHaveBeenCalled()
498
+ })
499
+ })
500
+
501
+ describe('request handling and proxy functionality', () => {
502
+ let mockReq, mockRes
503
+
504
+ beforeEach(async () => {
505
+ await Web.init()
506
+ mockReq = createMockRequest()
507
+ mockRes = createMockResponse()
508
+
509
+ // Setup a test website
510
+ mockConfig.config.websites = {
511
+ 'example.com': {
512
+ domain: 'example.com',
513
+ path: '/var/candypack/example.com',
514
+ pid: 12345,
515
+ port: 3000,
516
+ cert: {
517
+ ssl: {
518
+ key: '/path/to/example.key',
519
+ cert: '/path/to/example.cert'
520
+ }
521
+ }
522
+ }
523
+ }
524
+
525
+ // Mock watcher to indicate process is running
526
+ Web['_Web__watcher'] = {12345: true}
527
+ })
528
+
529
+ test('should redirect HTTP requests to HTTPS', () => {
530
+ // Verify the basic setup first
531
+ expect(mockConfig.config.websites['example.com']).toBeDefined()
532
+ expect(mockConfig.config.websites['example.com'].pid).toBe(12345)
533
+ expect(Web['_Web__watcher'][12345]).toBe(true)
534
+
535
+ mockReq.headers.host = 'example.com'
536
+ mockReq.url = '/test-path'
537
+
538
+ Web.request(mockReq, mockRes, false)
539
+
540
+ expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
541
+ Location: 'https://example.com/test-path'
542
+ })
543
+ expect(mockRes.end).toHaveBeenCalled()
544
+ })
545
+
546
+ test('should serve default index for requests without host header', () => {
547
+ mockReq.headers = {}
548
+
549
+ Web.request(mockReq, mockRes, true)
550
+
551
+ expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
552
+ expect(mockRes.end).toHaveBeenCalled()
553
+ })
554
+
555
+ test('should serve default index for unknown hosts', () => {
556
+ mockReq.headers.host = 'unknown.com'
557
+
558
+ Web.request(mockReq, mockRes, true)
559
+
560
+ expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
561
+ expect(mockRes.end).toHaveBeenCalled()
562
+ })
563
+
564
+ test('should resolve subdomain to parent domain', () => {
565
+ mockReq.headers.host = 'www.example.com'
566
+ mockReq.url = '/test'
567
+
568
+ Web.request(mockReq, mockRes, true)
569
+
570
+ expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
571
+ timeout: 30000,
572
+ proxyTimeout: 30000,
573
+ keepAlive: true
574
+ })
575
+ expect(mockProxyServer.web).toHaveBeenCalledWith(mockReq, mockRes, {
576
+ target: 'http://127.0.0.1:3000'
577
+ })
578
+ })
579
+
580
+ test('should proxy HTTPS requests to website process', () => {
581
+ mockReq.headers.host = 'example.com'
582
+ mockReq.url = '/api/test'
583
+
584
+ Web.request(mockReq, mockRes, true)
585
+
586
+ expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
587
+ timeout: 30000,
588
+ proxyTimeout: 30000,
589
+ keepAlive: true
590
+ })
591
+ expect(mockProxyServer.web).toHaveBeenCalledWith(mockReq, mockRes, {
592
+ target: 'http://127.0.0.1:3000'
593
+ })
594
+ })
595
+
596
+ test('should serve default index when website process is not running', () => {
597
+ mockConfig.config.websites['example.com'].pid = null
598
+ mockReq.headers.host = 'example.com'
599
+
600
+ Web.request(mockReq, mockRes, true)
601
+
602
+ expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
603
+ expect(mockRes.end).toHaveBeenCalled()
604
+ expect(httpProxy.createProxyServer).not.toHaveBeenCalled()
605
+ })
606
+
607
+ test('should serve default index when watcher indicates process is not running', () => {
608
+ Web['_Web__watcher'] = {12345: false}
609
+ mockReq.headers.host = 'example.com'
610
+
611
+ Web.request(mockReq, mockRes, true)
612
+
613
+ expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
614
+ expect(mockRes.end).toHaveBeenCalled()
615
+ expect(httpProxy.createProxyServer).not.toHaveBeenCalled()
616
+ })
617
+
618
+ test('should add custom headers to proxied requests', () => {
619
+ mockReq.headers.host = 'example.com'
620
+ mockReq.socket = {remoteAddress: '192.168.1.100'}
621
+
622
+ Web.request(mockReq, mockRes, true)
623
+
624
+ // Simulate proxyReq event
625
+ const proxyReqHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'proxyReq')[1]
626
+ const mockProxyReq = {
627
+ setHeader: jest.fn()
628
+ }
629
+
630
+ proxyReqHandler(mockProxyReq, mockReq)
631
+
632
+ expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-RemoteAddress', '192.168.1.100')
633
+ expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-SSL', 'true')
634
+ })
635
+
636
+ test('should handle proxy errors gracefully', () => {
637
+ mockReq.headers.host = 'example.com'
638
+
639
+ Web.request(mockReq, mockRes, true)
640
+
641
+ // Simulate proxy error
642
+ const errorHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'error')[1]
643
+ const mockError = new Error('Connection refused')
644
+
645
+ errorHandler(mockError, mockReq, mockRes)
646
+
647
+ expect(mockLog).toHaveBeenCalledWith('Proxy error for example.com: Connection refused')
648
+ expect(mockRes.statusCode).toBe(502)
649
+ expect(mockRes.end).toHaveBeenCalledWith('Bad Gateway')
650
+ })
651
+
652
+ test('should not set response status if headers already sent', () => {
653
+ mockReq.headers.host = 'example.com'
654
+ mockRes.headersSent = true
655
+
656
+ Web.request(mockReq, mockRes, true)
657
+
658
+ // Simulate proxy error
659
+ const errorHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'error')[1]
660
+ const mockError = new Error('Connection refused')
661
+
662
+ errorHandler(mockError, mockReq, mockRes)
663
+
664
+ expect(mockRes.statusCode).not.toBe(502)
665
+ expect(mockRes.end).not.toHaveBeenCalledWith('Bad Gateway')
666
+ })
667
+
668
+ test('should handle exceptions in request processing', () => {
669
+ mockReq.headers.host = 'example.com'
670
+ httpProxy.createProxyServer.mockImplementation(() => {
671
+ throw new Error('Proxy creation failed')
672
+ })
673
+
674
+ Web.request(mockReq, mockRes, true)
675
+
676
+ expect(mockLog).toHaveBeenCalledWith(expect.any(Error))
677
+ expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
678
+ expect(mockRes.end).toHaveBeenCalled()
679
+ })
680
+
681
+ test('should handle HTTP requests with query parameters in redirection', () => {
682
+ mockReq.headers.host = 'example.com'
683
+ mockReq.url = '/test-path?param=value&other=123'
684
+
685
+ Web.request(mockReq, mockRes, false)
686
+
687
+ expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
688
+ Location: 'https://example.com/test-path?param=value&other=123'
689
+ })
690
+ expect(mockRes.end).toHaveBeenCalled()
691
+ })
692
+
693
+ test('should handle HTTP requests with fragments in redirection', () => {
694
+ mockReq.headers.host = 'example.com'
695
+ mockReq.url = '/test-path#section'
696
+
697
+ Web.request(mockReq, mockRes, false)
698
+
699
+ expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
700
+ Location: 'https://example.com/test-path#section'
701
+ })
702
+ expect(mockRes.end).toHaveBeenCalled()
703
+ })
704
+
705
+ test('should handle multi-level subdomain resolution', () => {
706
+ // Setup a multi-level subdomain scenario
707
+ mockConfig.config.websites = {
708
+ 'example.com': {
709
+ domain: 'example.com',
710
+ path: '/var/candypack/example.com',
711
+ pid: 12345,
712
+ port: 3000
713
+ }
714
+ }
715
+ Web['_Web__watcher'] = {12345: true}
716
+
717
+ mockReq.headers.host = 'api.staging.example.com'
718
+ mockReq.url = '/test'
719
+
720
+ Web.request(mockReq, mockRes, true)
721
+
722
+ expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
723
+ timeout: 30000,
724
+ proxyTimeout: 30000,
725
+ keepAlive: true
726
+ })
727
+ expect(mockProxyServer.web).toHaveBeenCalledWith(mockReq, mockRes, {
728
+ target: 'http://127.0.0.1:3000'
729
+ })
730
+ })
731
+
732
+ test('should handle requests with port numbers in host header', () => {
733
+ mockReq.headers.host = 'example.com:8080'
734
+ mockReq.url = '/test'
735
+
736
+ Web.request(mockReq, mockRes, true)
737
+
738
+ expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
739
+ timeout: 30000,
740
+ proxyTimeout: 30000,
741
+ keepAlive: true
742
+ })
743
+ expect(mockProxyServer.web).toHaveBeenCalledWith(mockReq, mockRes, {
744
+ target: 'http://127.0.0.1:3000'
745
+ })
746
+ })
747
+
748
+ test('should set correct SSL header for HTTP requests', () => {
749
+ mockReq.headers.host = 'example.com'
750
+ mockReq.socket = {remoteAddress: '192.168.1.100'}
751
+
752
+ Web.request(mockReq, mockRes, false)
753
+
754
+ // HTTP request should redirect, but let's test the header logic by mocking a proxy scenario
755
+ // Reset mocks and test HTTPS request
756
+ jest.clearAllMocks()
757
+
758
+ Web.request(mockReq, mockRes, true)
759
+
760
+ // Simulate proxyReq event for HTTPS
761
+ const proxyReqHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'proxyReq')[1]
762
+ const mockProxyReq = {
763
+ setHeader: jest.fn()
764
+ }
765
+
766
+ proxyReqHandler(mockProxyReq, mockReq)
767
+
768
+ expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-SSL', 'true')
769
+ })
770
+
771
+ test('should handle missing remote address in proxy headers', () => {
772
+ mockReq.headers.host = 'example.com'
773
+ mockReq.socket = {} // No remoteAddress property
774
+
775
+ Web.request(mockReq, mockRes, true)
776
+
777
+ // Simulate proxyReq event
778
+ const proxyReqHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'proxyReq')[1]
779
+ const mockProxyReq = {
780
+ setHeader: jest.fn()
781
+ }
782
+
783
+ proxyReqHandler(mockProxyReq, mockReq)
784
+
785
+ expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-RemoteAddress', '')
786
+ expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-SSL', 'true')
787
+ })
788
+
789
+ test('should handle proxy timeout configuration', () => {
790
+ mockReq.headers.host = 'example.com'
791
+
792
+ Web.request(mockReq, mockRes, true)
793
+
794
+ expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
795
+ timeout: 30000,
796
+ proxyTimeout: 30000,
797
+ keepAlive: true
798
+ })
799
+ })
800
+ })
801
+
802
+ describe('process management and monitoring', () => {
803
+ let mockChild
804
+
805
+ beforeEach(async () => {
806
+ await Web.init()
807
+ mockConfig.config.web = {path: '/var/candypack'}
808
+
809
+ // Setup mock child process
810
+ mockChild = {
811
+ pid: 12345,
812
+ stdout: {on: jest.fn()},
813
+ stderr: {on: jest.fn()},
814
+ on: jest.fn()
815
+ }
816
+ childProcess.spawn.mockReturnValue(mockChild)
817
+
818
+ // Initialize Web module's private properties
819
+ Web['_Web__active'] = {}
820
+ Web['_Web__error_counts'] = {}
821
+ Web['_Web__logs'] = {log: {}, err: {}}
822
+ Web['_Web__ports'] = {}
823
+ Web['_Web__started'] = {}
824
+ Web['_Web__watcher'] = {}
825
+ })
826
+
827
+ test('should test port checking functionality', async () => {
828
+ const mockNetServer = {
829
+ once: jest.fn((event, callback) => {
830
+ if (event === 'listening') {
831
+ setTimeout(() => callback(), 0)
832
+ }
833
+ }),
834
+ listen: jest.fn(),
835
+ close: jest.fn()
836
+ }
837
+ net.createServer.mockReturnValue(mockNetServer)
838
+
839
+ const result = await Web.checkPort(3000)
840
+
841
+ expect(result).toBe(true)
842
+ expect(mockNetServer.listen).toHaveBeenCalledWith(3000, '127.0.0.1')
843
+ expect(mockNetServer.close).toHaveBeenCalled()
844
+ })
845
+
846
+ test('should detect port conflicts', async () => {
847
+ const mockNetServer = {
848
+ once: jest.fn((event, callback) => {
849
+ if (event === 'error') {
850
+ setTimeout(() => callback(), 0)
851
+ }
852
+ }),
853
+ listen: jest.fn(),
854
+ close: jest.fn()
855
+ }
856
+ net.createServer.mockReturnValue(mockNetServer)
857
+
858
+ const result = await Web.checkPort(3000)
859
+
860
+ expect(result).toBe(false)
861
+ })
862
+
863
+ test('should not start process if already active', async () => {
864
+ const domain = 'example.com'
865
+ mockConfig.config.websites = {
866
+ [domain]: {
867
+ domain,
868
+ path: '/var/candypack/example.com'
869
+ }
870
+ }
871
+
872
+ // Mark domain as active
873
+ Web['_Web__active'][domain] = true
874
+
875
+ await Web.start(domain)
876
+
877
+ expect(childProcess.spawn).not.toHaveBeenCalled()
878
+ })
879
+
880
+ test('should not start process if website does not exist', async () => {
881
+ await Web.start('nonexistent.com')
882
+
883
+ expect(childProcess.spawn).not.toHaveBeenCalled()
884
+ })
885
+
886
+ test('should respect error cooldown period', async () => {
887
+ const domain = 'example.com'
888
+ const now = Date.now()
889
+ mockConfig.config.websites = {
890
+ [domain]: {
891
+ domain,
892
+ path: '/var/candypack/example.com',
893
+ status: 'errored',
894
+ updated: now - 500 // 500ms ago
895
+ }
896
+ }
897
+
898
+ // Set error count to 2 (should wait 2 seconds)
899
+ Web['_Web__error_counts'][domain] = 2
900
+
901
+ await Web.start(domain)
902
+
903
+ expect(childProcess.spawn).not.toHaveBeenCalled()
904
+ })
905
+
906
+ test('should not start process without index.js file', async () => {
907
+ const domain = 'example.com'
908
+ mockConfig.config.websites = {
909
+ [domain]: {
910
+ domain,
911
+ path: '/var/candypack/example.com'
912
+ }
913
+ }
914
+
915
+ // Mock index.js file as missing
916
+ fs.existsSync.mockImplementation(path => !path.includes('index.js'))
917
+
918
+ await Web.start(domain)
919
+
920
+ expect(childProcess.spawn).not.toHaveBeenCalled()
921
+ expect(mockLog).toHaveBeenCalledWith("Website example.com doesn't have index.js file.")
922
+ })
923
+
924
+ test('should automatically restart crashed processes via check method', async () => {
925
+ const domain = 'example.com'
926
+ mockConfig.config.websites = {
927
+ [domain]: {
928
+ domain,
929
+ path: '/var/candypack/example.com',
930
+ pid: null // No process running
931
+ }
932
+ }
933
+
934
+ // Spy on the start method
935
+ const startSpy = jest.spyOn(Web, 'start')
936
+
937
+ Web.check()
938
+
939
+ expect(startSpy).toHaveBeenCalledWith(domain)
940
+ })
941
+
942
+ test('should restart processes when watcher indicates they are not running', async () => {
943
+ const domain = 'example.com'
944
+ const pid = 12345
945
+ mockConfig.config.websites = {
946
+ [domain]: {
947
+ domain,
948
+ path: '/var/candypack/example.com',
949
+ pid
950
+ }
951
+ }
952
+
953
+ // Mark process as not running in watcher
954
+ Web['_Web__watcher'][pid] = false
955
+
956
+ const mockProcess = {
957
+ stop: jest.fn()
958
+ }
959
+ mockCandy.setMock('core', 'Process', mockProcess)
960
+
961
+ const startSpy = jest.spyOn(Web, 'start')
962
+
963
+ Web.check()
964
+
965
+ expect(mockProcess.stop).toHaveBeenCalledWith(pid)
966
+ expect(mockConfig.config.websites[domain].pid).toBeNull()
967
+ expect(startSpy).toHaveBeenCalledWith(domain)
968
+ })
969
+
970
+ test('should write logs to files during check', async () => {
971
+ const domain = 'example.com'
972
+ mockConfig.config.websites = {
973
+ [domain]: {
974
+ domain,
975
+ path: '/var/candypack/example.com',
976
+ pid: 12345
977
+ }
978
+ }
979
+
980
+ // Setup logs
981
+ Web['_Web__logs'].log[domain] = 'Test log content'
982
+ Web['_Web__logs'].err[domain] = 'Test error content'
983
+ Web['_Web__watcher'][12345] = true
984
+
985
+ os.homedir.mockReturnValue('/home/user')
986
+
987
+ Web.check()
988
+
989
+ expect(fs.writeFile).toHaveBeenCalledWith('/home/user/.candypack/logs/example.com.log', 'Test log content', expect.any(Function))
990
+ expect(fs.writeFile).toHaveBeenCalledWith('/var/candypack/example.com/error.log', 'Test error content', expect.any(Function))
991
+ })
992
+
993
+ test('should handle log file write errors gracefully', async () => {
994
+ const domain = 'example.com'
995
+ mockConfig.config.websites = {
996
+ [domain]: {
997
+ domain,
998
+ path: '/var/candypack/example.com',
999
+ pid: 12345
1000
+ }
1001
+ }
1002
+
1003
+ Web['_Web__logs'].log[domain] = 'Test log content'
1004
+ Web['_Web__watcher'][12345] = true
1005
+
1006
+ // Mock fs.writeFile to call callback with error
1007
+ fs.writeFile.mockImplementation((path, data, callback) => {
1008
+ callback(new Error('Write failed'))
1009
+ })
1010
+
1011
+ Web.check()
1012
+
1013
+ // Should not throw, error should be logged
1014
+ expect(mockLog).toHaveBeenCalledWith(expect.any(Error))
1015
+ })
1016
+ })
1017
+
1018
+ describe('website deletion and resource cleanup', () => {
1019
+ beforeEach(async () => {
1020
+ await Web.init()
1021
+ })
1022
+
1023
+ test('should delete website and cleanup all resources', async () => {
1024
+ const domain = 'example.com'
1025
+ const pid = 12345
1026
+ const port = 60000
1027
+
1028
+ mockConfig.config.websites = {
1029
+ [domain]: {
1030
+ domain,
1031
+ path: '/var/candypack/example.com',
1032
+ pid,
1033
+ port
1034
+ }
1035
+ }
1036
+
1037
+ // Setup internal state
1038
+ Web['_Web__watcher'][pid] = true
1039
+ Web['_Web__ports'][port] = true
1040
+ Web['_Web__logs'].log[domain] = 'log content'
1041
+ Web['_Web__logs'].err[domain] = 'error content'
1042
+ Web['_Web__error_counts'][domain] = 2
1043
+ Web['_Web__active'][domain] = false
1044
+ Web['_Web__started'][domain] = Date.now()
1045
+
1046
+ const mockProcess = {
1047
+ stop: jest.fn()
1048
+ }
1049
+ mockCandy.setMock('core', 'Process', mockProcess)
1050
+
1051
+ const result = await Web.delete(domain)
1052
+
1053
+ expect(result.success).toBe(true)
1054
+ expect(result.message).toContain('Website example.com deleted')
1055
+ expect(mockConfig.config.websites[domain]).toBeUndefined()
1056
+ expect(mockProcess.stop).toHaveBeenCalledWith(pid)
1057
+ expect(Web['_Web__watcher'][pid]).toBeUndefined()
1058
+ expect(Web['_Web__ports'][port]).toBeUndefined()
1059
+ expect(Web['_Web__logs'].log[domain]).toBeUndefined()
1060
+ expect(Web['_Web__logs'].err[domain]).toBeUndefined()
1061
+ expect(Web['_Web__error_counts'][domain]).toBeUndefined()
1062
+ expect(Web['_Web__active'][domain]).toBeUndefined()
1063
+ expect(Web['_Web__started'][domain]).toBeUndefined()
1064
+ })
1065
+
1066
+ test('should handle deletion of non-existent website', async () => {
1067
+ const result = await Web.delete('nonexistent.com')
1068
+
1069
+ expect(result.success).toBe(false)
1070
+ expect(result.message).toContain('Website nonexistent.com not found')
1071
+ })
1072
+
1073
+ test('should handle deletion of website without running process', async () => {
1074
+ const domain = 'example.com'
1075
+
1076
+ mockConfig.config.websites = {
1077
+ [domain]: {
1078
+ domain,
1079
+ path: '/var/candypack/example.com',
1080
+ pid: null // No process running
1081
+ }
1082
+ }
1083
+
1084
+ const result = await Web.delete(domain)
1085
+
1086
+ expect(result.success).toBe(true)
1087
+ expect(result.message).toContain('Website example.com deleted')
1088
+ expect(mockConfig.config.websites[domain]).toBeUndefined()
1089
+ })
1090
+
1091
+ test('should strip protocol prefixes from domain in deletion', async () => {
1092
+ const domain = 'example.com'
1093
+
1094
+ mockConfig.config.websites = {
1095
+ [domain]: {
1096
+ domain,
1097
+ path: '/var/candypack/example.com'
1098
+ }
1099
+ }
1100
+
1101
+ const result = await Web.delete('https://example.com')
1102
+
1103
+ expect(result.success).toBe(true)
1104
+ expect(mockConfig.config.websites[domain]).toBeUndefined()
1105
+ })
1106
+
1107
+ test('should stop all website processes via stopAll method', () => {
1108
+ const domain1 = 'example.com'
1109
+ const domain2 = 'test.com'
1110
+ const pid1 = 12345
1111
+ const pid2 = 67890
1112
+
1113
+ mockConfig.config.websites = {
1114
+ [domain1]: {domain: domain1, pid: pid1},
1115
+ [domain2]: {domain: domain2, pid: pid2}
1116
+ }
1117
+
1118
+ const mockProcess = {
1119
+ stop: jest.fn()
1120
+ }
1121
+ mockCandy.setMock('core', 'Process', mockProcess)
1122
+
1123
+ Web.stopAll()
1124
+
1125
+ expect(mockProcess.stop).toHaveBeenCalledWith(pid1)
1126
+ expect(mockProcess.stop).toHaveBeenCalledWith(pid2)
1127
+ expect(mockConfig.config.websites[domain1].pid).toBeNull()
1128
+ expect(mockConfig.config.websites[domain2].pid).toBeNull()
1129
+ })
1130
+
1131
+ test('should handle stopAll with no websites', () => {
1132
+ mockConfig.config.websites = {}
1133
+
1134
+ const mockProcess = {
1135
+ stop: jest.fn()
1136
+ }
1137
+ mockCandy.setMock('core', 'Process', mockProcess)
1138
+
1139
+ expect(() => Web.stopAll()).not.toThrow()
1140
+ expect(mockProcess.stop).not.toHaveBeenCalled()
1141
+ })
1142
+
1143
+ test('should handle stopAll with websites that have no running processes', () => {
1144
+ const domain = 'example.com'
1145
+
1146
+ mockConfig.config.websites = {
1147
+ [domain]: {domain, pid: null}
1148
+ }
1149
+
1150
+ const mockProcess = {
1151
+ stop: jest.fn()
1152
+ }
1153
+ mockCandy.setMock('core', 'Process', mockProcess)
1154
+
1155
+ Web.stopAll()
1156
+
1157
+ expect(mockProcess.stop).not.toHaveBeenCalled()
1158
+ })
1159
+ })
1160
+
1161
+ describe('SSL certificate handling and SNI', () => {
1162
+ beforeEach(async () => {
1163
+ await Web.init()
1164
+ mockConfig.config.ssl = {
1165
+ key: '/path/to/default.key',
1166
+ cert: '/path/to/default.cert'
1167
+ }
1168
+ mockConfig.config.websites = {
1169
+ 'example.com': {
1170
+ domain: 'example.com',
1171
+ cert: {
1172
+ ssl: {
1173
+ key: '/path/to/example.key',
1174
+ cert: '/path/to/example.cert'
1175
+ }
1176
+ }
1177
+ },
1178
+ 'test.com': {
1179
+ domain: 'test.com',
1180
+ cert: false
1181
+ }
1182
+ }
1183
+ })
1184
+
1185
+ test('should use website-specific SSL certificate via SNI', () => {
1186
+ Web.server()
1187
+
1188
+ const httpsOptions = https.createServer.mock.calls[0][0]
1189
+ const sniCallback = httpsOptions.SNICallback
1190
+ const mockCallback = jest.fn()
1191
+
1192
+ sniCallback('example.com', mockCallback)
1193
+
1194
+ expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
1195
+ expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
1196
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1197
+ key: 'mock-file-content',
1198
+ cert: 'mock-file-content'
1199
+ })
1200
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1201
+ })
1202
+
1203
+ test('should fall back to default SSL certificate for websites without specific certs', () => {
1204
+ Web.server()
1205
+
1206
+ const httpsOptions = https.createServer.mock.calls[0][0]
1207
+ const sniCallback = httpsOptions.SNICallback
1208
+ const mockCallback = jest.fn()
1209
+
1210
+ sniCallback('test.com', mockCallback)
1211
+
1212
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1213
+ key: 'mock-file-content',
1214
+ cert: 'mock-file-content'
1215
+ })
1216
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1217
+ })
1218
+
1219
+ test('should resolve subdomain to parent domain for SSL certificate', () => {
1220
+ Web.server()
1221
+
1222
+ const httpsOptions = https.createServer.mock.calls[0][0]
1223
+ const sniCallback = httpsOptions.SNICallback
1224
+ const mockCallback = jest.fn()
1225
+
1226
+ sniCallback('www.example.com', mockCallback)
1227
+
1228
+ expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
1229
+ expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
1230
+ })
1231
+
1232
+ test('should use default certificate for unknown domains', () => {
1233
+ Web.server()
1234
+
1235
+ const httpsOptions = https.createServer.mock.calls[0][0]
1236
+ const sniCallback = httpsOptions.SNICallback
1237
+ const mockCallback = jest.fn()
1238
+
1239
+ sniCallback('unknown.com', mockCallback)
1240
+
1241
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1242
+ key: 'mock-file-content',
1243
+ cert: 'mock-file-content'
1244
+ })
1245
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1246
+ })
1247
+
1248
+ test('should handle SSL certificate file read errors', () => {
1249
+ fs.readFileSync.mockImplementation(path => {
1250
+ if (path.includes('example.key')) {
1251
+ throw new Error('File not found')
1252
+ }
1253
+ return 'mock-file-content'
1254
+ })
1255
+
1256
+ Web.server()
1257
+
1258
+ const httpsOptions = https.createServer.mock.calls[0][0]
1259
+ const sniCallback = httpsOptions.SNICallback
1260
+ const mockCallback = jest.fn()
1261
+
1262
+ sniCallback('example.com', mockCallback)
1263
+
1264
+ expect(mockLog).toHaveBeenCalledWith('SSL certificate error for example.com: File not found')
1265
+ expect(mockCallback).toHaveBeenCalledWith(expect.any(Error))
1266
+ })
1267
+
1268
+ test('should handle missing SSL certificate files gracefully', () => {
1269
+ fs.existsSync.mockImplementation(path => !path.includes('example.key') && !path.includes('example.cert'))
1270
+
1271
+ Web.server()
1272
+
1273
+ const httpsOptions = https.createServer.mock.calls[0][0]
1274
+ const sniCallback = httpsOptions.SNICallback
1275
+ const mockCallback = jest.fn()
1276
+
1277
+ sniCallback('example.com', mockCallback)
1278
+
1279
+ // Should fall back to default certificate
1280
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1281
+ key: 'mock-file-content',
1282
+ cert: 'mock-file-content'
1283
+ })
1284
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1285
+ })
1286
+
1287
+ test('should handle multi-level subdomain SSL certificate resolution', () => {
1288
+ Web.server()
1289
+
1290
+ const httpsOptions = https.createServer.mock.calls[0][0]
1291
+ const sniCallback = httpsOptions.SNICallback
1292
+ const mockCallback = jest.fn()
1293
+
1294
+ // Test multi-level subdomain resolution (api.staging.example.com -> example.com)
1295
+ sniCallback('api.staging.example.com', mockCallback)
1296
+
1297
+ expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
1298
+ expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
1299
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1300
+ key: 'mock-file-content',
1301
+ cert: 'mock-file-content'
1302
+ })
1303
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1304
+ })
1305
+
1306
+ test('should handle SSL certificate with missing cert property', () => {
1307
+ mockConfig.config.websites['example.com'].cert = {
1308
+ ssl: {
1309
+ key: '/path/to/example.key'
1310
+ // Missing cert property
1311
+ }
1312
+ }
1313
+
1314
+ Web.server()
1315
+
1316
+ const httpsOptions = https.createServer.mock.calls[0][0]
1317
+ const sniCallback = httpsOptions.SNICallback
1318
+ const mockCallback = jest.fn()
1319
+
1320
+ sniCallback('example.com', mockCallback)
1321
+
1322
+ // Should fall back to default certificate
1323
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1324
+ key: 'mock-file-content',
1325
+ cert: 'mock-file-content'
1326
+ })
1327
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1328
+ })
1329
+
1330
+ test('should handle SSL certificate with missing key property', () => {
1331
+ mockConfig.config.websites['example.com'].cert = {
1332
+ ssl: {
1333
+ cert: '/path/to/example.cert'
1334
+ // Missing key property
1335
+ }
1336
+ }
1337
+
1338
+ Web.server()
1339
+
1340
+ const httpsOptions = https.createServer.mock.calls[0][0]
1341
+ const sniCallback = httpsOptions.SNICallback
1342
+ const mockCallback = jest.fn()
1343
+
1344
+ sniCallback('example.com', mockCallback)
1345
+
1346
+ // Should fall back to default certificate
1347
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1348
+ key: 'mock-file-content',
1349
+ cert: 'mock-file-content'
1350
+ })
1351
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1352
+ })
1353
+
1354
+ test('should handle SSL certificate with missing ssl property', () => {
1355
+ mockConfig.config.websites['example.com'].cert = {
1356
+ // Missing ssl property
1357
+ }
1358
+
1359
+ Web.server()
1360
+
1361
+ const httpsOptions = https.createServer.mock.calls[0][0]
1362
+ const sniCallback = httpsOptions.SNICallback
1363
+ const mockCallback = jest.fn()
1364
+
1365
+ sniCallback('example.com', mockCallback)
1366
+
1367
+ // Should fall back to default certificate
1368
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1369
+ key: 'mock-file-content',
1370
+ cert: 'mock-file-content'
1371
+ })
1372
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1373
+ })
1374
+
1375
+ test('should handle hostname without dots in SNI callback', () => {
1376
+ Web.server()
1377
+
1378
+ const httpsOptions = https.createServer.mock.calls[0][0]
1379
+ const sniCallback = httpsOptions.SNICallback
1380
+ const mockCallback = jest.fn()
1381
+
1382
+ sniCallback('localhost', mockCallback)
1383
+
1384
+ // Should use default certificate for localhost
1385
+ expect(tls.createSecureContext).toHaveBeenCalledWith({
1386
+ key: 'mock-file-content',
1387
+ cert: 'mock-file-content'
1388
+ })
1389
+ expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1390
+ })
1391
+
1392
+ test('should handle tls.createSecureContext errors', () => {
1393
+ tls.createSecureContext.mockImplementation(() => {
1394
+ throw new Error('Invalid certificate format')
1395
+ })
1396
+
1397
+ Web.server()
1398
+
1399
+ const httpsOptions = https.createServer.mock.calls[0][0]
1400
+ const sniCallback = httpsOptions.SNICallback
1401
+ const mockCallback = jest.fn()
1402
+
1403
+ sniCallback('example.com', mockCallback)
1404
+
1405
+ expect(mockLog).toHaveBeenCalledWith('SSL certificate error for example.com: Invalid certificate format')
1406
+ expect(mockCallback).toHaveBeenCalledWith(expect.any(Error))
1407
+ })
1408
+ })
1409
+
1410
+ describe('website deletion', () => {
1411
+ beforeEach(async () => {
1412
+ await Web.init()
1413
+ mockConfig.config.websites = {
1414
+ 'example.com': {
1415
+ domain: 'example.com',
1416
+ path: '/var/candypack/example.com',
1417
+ pid: 12345,
1418
+ port: 3000
1419
+ }
1420
+ }
1421
+ Web['_Web__watcher'] = {12345: true}
1422
+ Web['_Web__ports'] = {3000: true}
1423
+ Web['_Web__logs'] = {
1424
+ log: {'example.com': 'log content'},
1425
+ err: {'example.com': 'error content'}
1426
+ }
1427
+ Web['_Web__error_counts'] = {'example.com': 2}
1428
+ Web['_Web__active'] = {'example.com': false}
1429
+ Web['_Web__started'] = {'example.com': Date.now()}
1430
+ })
1431
+
1432
+ test('should delete website and cleanup all resources', async () => {
1433
+ const mockProcess = {
1434
+ stop: jest.fn()
1435
+ }
1436
+ mockCandy.setMock('core', 'Process', mockProcess)
1437
+
1438
+ const result = await Web.delete('example.com')
1439
+
1440
+ expect(result.success).toBe(true)
1441
+ expect(result.message).toBe('Website example.com deleted.')
1442
+ expect(mockConfig.config.websites['example.com']).toBeUndefined()
1443
+ expect(mockProcess.stop).toHaveBeenCalledWith(12345)
1444
+ expect(Web['_Web__watcher'][12345]).toBeUndefined()
1445
+ expect(Web['_Web__ports'][3000]).toBeUndefined()
1446
+ expect(Web['_Web__logs'].log['example.com']).toBeUndefined()
1447
+ expect(Web['_Web__logs'].err['example.com']).toBeUndefined()
1448
+ expect(Web['_Web__error_counts']['example.com']).toBeUndefined()
1449
+ expect(Web['_Web__active']['example.com']).toBeUndefined()
1450
+ expect(Web['_Web__started']['example.com']).toBeUndefined()
1451
+ })
1452
+
1453
+ test('should strip protocol prefixes before deletion', async () => {
1454
+ const result = await Web.delete('https://example.com')
1455
+
1456
+ expect(result.success).toBe(true)
1457
+ expect(mockConfig.config.websites['example.com']).toBeUndefined()
1458
+ })
1459
+
1460
+ test('should return error for non-existent website', async () => {
1461
+ const result = await Web.delete('nonexistent.com')
1462
+
1463
+ expect(result.success).toBe(false)
1464
+ expect(result.message).toBe('Website nonexistent.com not found.')
1465
+ })
1466
+
1467
+ test('should handle deletion of website without running process', async () => {
1468
+ mockConfig.config.websites['example.com'].pid = null
1469
+
1470
+ const result = await Web.delete('example.com')
1471
+
1472
+ expect(result.success).toBe(true)
1473
+ expect(mockConfig.config.websites['example.com']).toBeUndefined()
1474
+ })
1475
+ })
1476
+
1477
+ describe('utility methods', () => {
1478
+ beforeEach(async () => {
1479
+ await Web.init()
1480
+ })
1481
+
1482
+ test('should list all websites', async () => {
1483
+ mockConfig.config.websites = {
1484
+ 'example.com': {},
1485
+ 'test.com': {},
1486
+ 'demo.com': {}
1487
+ }
1488
+
1489
+ const result = await Web.list()
1490
+
1491
+ expect(result.success).toBe(true)
1492
+ expect(result.message).toContain('example.com')
1493
+ expect(result.message).toContain('test.com')
1494
+ expect(result.message).toContain('demo.com')
1495
+ })
1496
+
1497
+ test('should return error when no websites exist', async () => {
1498
+ mockConfig.config.websites = {}
1499
+
1500
+ const result = await Web.list()
1501
+
1502
+ expect(result.success).toBe(false)
1503
+ expect(result.message).toBe('No websites found.')
1504
+ })
1505
+
1506
+ test('should return website status', async () => {
1507
+ mockConfig.config.websites = {
1508
+ 'example.com': {
1509
+ status: 'running',
1510
+ pid: 12345
1511
+ }
1512
+ }
1513
+
1514
+ const result = await Web.status()
1515
+
1516
+ expect(result).toEqual(mockConfig.config.websites)
1517
+ })
1518
+
1519
+ test('should set website configuration', () => {
1520
+ const websiteData = {
1521
+ domain: 'example.com',
1522
+ path: '/var/candypack/example.com',
1523
+ status: 'running'
1524
+ }
1525
+
1526
+ Web.set('example.com', websiteData)
1527
+
1528
+ expect(mockConfig.config.websites['example.com']).toEqual(websiteData)
1529
+ })
1530
+
1531
+ test('should stop all websites', () => {
1532
+ mockConfig.config.websites = {
1533
+ 'example.com': {pid: 12345},
1534
+ 'test.com': {pid: 67890},
1535
+ 'demo.com': {pid: null}
1536
+ }
1537
+
1538
+ const mockProcess = {
1539
+ stop: jest.fn()
1540
+ }
1541
+ mockCandy.setMock('core', 'Process', mockProcess)
1542
+
1543
+ Web.stopAll()
1544
+
1545
+ expect(mockProcess.stop).toHaveBeenCalledWith(12345)
1546
+ expect(mockProcess.stop).toHaveBeenCalledWith(67890)
1547
+ expect(mockProcess.stop).toHaveBeenCalledTimes(2)
1548
+ expect(mockConfig.config.websites['example.com'].pid).toBe(null)
1549
+ expect(mockConfig.config.websites['test.com'].pid).toBe(null)
1550
+ })
1551
+
1552
+ test('should serve default index page', () => {
1553
+ const mockReq = createMockRequest()
1554
+ const mockRes = createMockResponse()
1555
+
1556
+ Web.index(mockReq, mockRes)
1557
+
1558
+ expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
1559
+ expect(mockRes.end).toHaveBeenCalled()
1560
+ })
1561
+ })
1562
+ })