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,765 @@
1
+ // Create a test version of the Server class
2
+ class TestServer {
3
+ constructor() {
4
+ this.mockConfig = {
5
+ server: {
6
+ pid: null,
7
+ started: null
8
+ }
9
+ }
10
+
11
+ this.mockService = {
12
+ check: jest.fn(),
13
+ stopAll: jest.fn()
14
+ }
15
+
16
+ this.mockDNS = {}
17
+ this.mockWeb = {
18
+ check: jest.fn(),
19
+ stopAll: jest.fn()
20
+ }
21
+
22
+ this.mockMail = {
23
+ check: jest.fn()
24
+ }
25
+
26
+ this.mockApi = {}
27
+ this.mockSSL = {
28
+ check: jest.fn()
29
+ }
30
+
31
+ // Store original global Candy if it exists
32
+ this.originalCandy = global.Candy
33
+
34
+ // Setup isolated Candy mock for this instance
35
+ global.Candy = {
36
+ core: jest.fn(module => {
37
+ if (module === 'Config') {
38
+ return {config: this.mockConfig}
39
+ }
40
+ return {}
41
+ }),
42
+ server: jest.fn(module => {
43
+ switch (module) {
44
+ case 'Service':
45
+ return this.mockService
46
+ case 'DNS':
47
+ return this.mockDNS
48
+ case 'Web':
49
+ return this.mockWeb
50
+ case 'Mail':
51
+ return this.mockMail
52
+ case 'Api':
53
+ return this.mockApi
54
+ case 'SSL':
55
+ return this.mockSSL
56
+ default:
57
+ return {}
58
+ }
59
+ })
60
+ }
61
+
62
+ // Initialize like the real Server
63
+ this.init()
64
+ }
65
+
66
+ init() {
67
+ global.Candy.core('Config').config.server.pid = process.pid
68
+ global.Candy.core('Config').config.server.started = Date.now()
69
+ global.Candy.server('Service')
70
+ global.Candy.server('DNS')
71
+ global.Candy.server('Web')
72
+ global.Candy.server('Mail')
73
+ global.Candy.server('Api')
74
+
75
+ // Setup health checks
76
+ setTimeout(() => {
77
+ this.intervalId = setInterval(() => {
78
+ try {
79
+ global.Candy.server('Service').check()
80
+ } catch (e) {
81
+ // Ignore service check errors
82
+ }
83
+ try {
84
+ global.Candy.server('SSL').check()
85
+ } catch (e) {
86
+ // Ignore SSL check errors
87
+ }
88
+ try {
89
+ global.Candy.server('Web').check()
90
+ } catch (e) {
91
+ // Ignore Web check errors
92
+ }
93
+ try {
94
+ global.Candy.server('Mail').check()
95
+ } catch (e) {
96
+ // Ignore Mail check errors
97
+ }
98
+ }, 1000)
99
+ }, 1000)
100
+ }
101
+
102
+ stop() {
103
+ try {
104
+ global.Candy.server('Service').stopAll()
105
+ } catch (e) {
106
+ // Ignore service stop errors
107
+ }
108
+ try {
109
+ global.Candy.server('Web').stopAll()
110
+ } catch (e) {
111
+ // Ignore web stop errors
112
+ }
113
+ if (this.intervalId) {
114
+ clearInterval(this.intervalId)
115
+ }
116
+ }
117
+
118
+ destroy() {
119
+ this.stop()
120
+ // Restore original Candy if it existed
121
+ if (this.originalCandy) {
122
+ global.Candy = this.originalCandy
123
+ } else {
124
+ delete global.Candy
125
+ }
126
+ }
127
+ }
128
+
129
+ describe('Server', () => {
130
+ let server
131
+
132
+ beforeEach(() => {
133
+ jest.clearAllMocks()
134
+ jest.clearAllTimers()
135
+ jest.useFakeTimers()
136
+
137
+ // Clear any existing global Candy
138
+ delete global.Candy
139
+ })
140
+
141
+ afterEach(() => {
142
+ if (server && server.destroy) {
143
+ server.destroy()
144
+ }
145
+ jest.useRealTimers()
146
+ delete global.Candy
147
+ })
148
+
149
+ describe('initialization', () => {
150
+ test('should set server PID in config during construction', () => {
151
+ const originalPid = process.pid
152
+
153
+ // Set the PID before creating the server
154
+ Object.defineProperty(process, 'pid', {
155
+ value: 12345,
156
+ writable: true
157
+ })
158
+
159
+ server = new TestServer()
160
+
161
+ expect(server.mockConfig.server.pid).toBe(12345)
162
+
163
+ // Restore original PID
164
+ Object.defineProperty(process, 'pid', {
165
+ value: originalPid,
166
+ writable: true
167
+ })
168
+ })
169
+
170
+ test('should set server started timestamp during construction', () => {
171
+ const mockDate = 1640995200000 // 2022-01-01 00:00:00
172
+ jest.spyOn(Date, 'now').mockReturnValue(mockDate)
173
+
174
+ server = new TestServer()
175
+
176
+ expect(server.mockConfig.server.started).toBe(mockDate)
177
+
178
+ Date.now.mockRestore()
179
+ })
180
+
181
+ test('should initialize all required services in correct order', () => {
182
+ server = new TestServer()
183
+
184
+ // Verify all services are initialized
185
+ expect(global.Candy.server).toHaveBeenCalledWith('Service')
186
+ expect(global.Candy.server).toHaveBeenCalledWith('DNS')
187
+ expect(global.Candy.server).toHaveBeenCalledWith('Web')
188
+ expect(global.Candy.server).toHaveBeenCalledWith('Mail')
189
+ expect(global.Candy.server).toHaveBeenCalledWith('Api')
190
+
191
+ // Verify call order
192
+ const serverCalls = global.Candy.server.mock.calls.map(call => call[0])
193
+ expect(serverCalls).toEqual(['Service', 'DNS', 'Web', 'Mail', 'Api'])
194
+ })
195
+
196
+ test('should setup periodic health checks after initialization delay', () => {
197
+ server = new TestServer()
198
+
199
+ // Initially no checks should be called
200
+ expect(server.mockService.check).not.toHaveBeenCalled()
201
+ expect(server.mockSSL.check).not.toHaveBeenCalled()
202
+ expect(server.mockWeb.check).not.toHaveBeenCalled()
203
+ expect(server.mockMail.check).not.toHaveBeenCalled()
204
+
205
+ // Fast-forward past initial delay
206
+ jest.advanceTimersByTime(1000)
207
+
208
+ // Still no checks after just the initial delay
209
+ expect(server.mockService.check).not.toHaveBeenCalled()
210
+
211
+ // Fast-forward to trigger first interval
212
+ jest.advanceTimersByTime(1000)
213
+
214
+ // Now checks should be called
215
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
216
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(1)
217
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(1)
218
+ expect(server.mockMail.check).toHaveBeenCalledTimes(1)
219
+ })
220
+
221
+ test('should continue periodic health checks at 1 second intervals', () => {
222
+ server = new TestServer()
223
+
224
+ // Fast-forward past initial delay and first interval
225
+ jest.advanceTimersByTime(2000)
226
+
227
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
228
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(1)
229
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(1)
230
+ expect(server.mockMail.check).toHaveBeenCalledTimes(1)
231
+
232
+ // Fast-forward another second
233
+ jest.advanceTimersByTime(1000)
234
+
235
+ expect(server.mockService.check).toHaveBeenCalledTimes(2)
236
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(2)
237
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(2)
238
+ expect(server.mockMail.check).toHaveBeenCalledTimes(2)
239
+
240
+ // Fast-forward multiple intervals
241
+ jest.advanceTimersByTime(3000)
242
+
243
+ expect(server.mockService.check).toHaveBeenCalledTimes(5)
244
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(5)
245
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(5)
246
+ expect(server.mockMail.check).toHaveBeenCalledTimes(5)
247
+ })
248
+ })
249
+
250
+ describe('service coordination', () => {
251
+ test('should handle service initialization errors gracefully', () => {
252
+ // Create a modified TestServer that throws during DNS initialization
253
+ class ErrorTestServer extends TestServer {
254
+ init() {
255
+ global.Candy.core('Config').config.server.pid = process.pid
256
+ global.Candy.core('Config').config.server.started = Date.now()
257
+ global.Candy.server('Service')
258
+
259
+ // This should throw
260
+ throw new Error('DNS initialization failed')
261
+ }
262
+ }
263
+
264
+ expect(() => {
265
+ new ErrorTestServer()
266
+ }).toThrow('DNS initialization failed')
267
+ })
268
+
269
+ test('should maintain service references for health checks', () => {
270
+ server = new TestServer()
271
+
272
+ // Fast-forward to trigger health checks
273
+ jest.advanceTimersByTime(2000)
274
+
275
+ // Verify that the same service instances are being called during health checks
276
+ expect(global.Candy.server).toHaveBeenCalledWith('Service')
277
+ expect(global.Candy.server).toHaveBeenCalledWith('SSL')
278
+ expect(global.Candy.server).toHaveBeenCalledWith('Web')
279
+ expect(global.Candy.server).toHaveBeenCalledWith('Mail')
280
+ })
281
+ })
282
+
283
+ describe('monitoring and health checks', () => {
284
+ beforeEach(() => {
285
+ server = new TestServer()
286
+ })
287
+
288
+ test('should perform health checks on all monitored services', () => {
289
+ // Fast-forward to trigger health checks
290
+ jest.advanceTimersByTime(2000)
291
+
292
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
293
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(1)
294
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(1)
295
+ expect(server.mockMail.check).toHaveBeenCalledTimes(1)
296
+ })
297
+
298
+ test('should handle health check errors without stopping monitoring', () => {
299
+ // Mock a service check that throws an error
300
+ server.mockService.check.mockImplementation(() => {
301
+ throw new Error('Service check failed')
302
+ })
303
+
304
+ // Should not throw when health check fails
305
+ expect(() => {
306
+ jest.advanceTimersByTime(2000)
307
+ }).not.toThrow()
308
+
309
+ // Other services should still be checked
310
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(1)
311
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(1)
312
+ expect(server.mockMail.check).toHaveBeenCalledTimes(1)
313
+ })
314
+
315
+ test('should continue monitoring after individual service failures', () => {
316
+ // Mock SSL check to fail on first call but succeed on second
317
+ let callCount = 0
318
+ server.mockSSL.check.mockImplementation(() => {
319
+ callCount++
320
+ if (callCount === 1) {
321
+ throw new Error('SSL check failed')
322
+ }
323
+ })
324
+
325
+ // First health check cycle
326
+ jest.advanceTimersByTime(2000)
327
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(1)
328
+
329
+ // Second health check cycle should still call SSL check
330
+ jest.advanceTimersByTime(1000)
331
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(2)
332
+ })
333
+
334
+ test('should maintain consistent monitoring intervals', () => {
335
+ const checkTimes = []
336
+ server.mockService.check.mockImplementation(() => {
337
+ checkTimes.push(Date.now())
338
+ })
339
+
340
+ // Trigger multiple health check cycles
341
+ jest.advanceTimersByTime(2000) // Initial delay + first check
342
+ jest.advanceTimersByTime(1000) // Second check
343
+ jest.advanceTimersByTime(1000) // Third check
344
+
345
+ expect(server.mockService.check).toHaveBeenCalledTimes(3)
346
+ })
347
+
348
+ test('should implement 1-second periodic health check intervals', () => {
349
+ // Verify initial delay before first health check
350
+ jest.advanceTimersByTime(999)
351
+ expect(server.mockService.check).not.toHaveBeenCalled()
352
+
353
+ // First health check after 1 second delay
354
+ jest.advanceTimersByTime(1)
355
+ expect(server.mockService.check).not.toHaveBeenCalled() // Still in initial delay
356
+
357
+ // Complete initial delay and first interval
358
+ jest.advanceTimersByTime(1000)
359
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
360
+
361
+ // Verify subsequent 1-second intervals
362
+ for (let i = 2; i <= 5; i++) {
363
+ jest.advanceTimersByTime(1000)
364
+ expect(server.mockService.check).toHaveBeenCalledTimes(i)
365
+ }
366
+ })
367
+
368
+ test('should monitor service status and health continuously', () => {
369
+ const serviceStatuses = []
370
+
371
+ // Track service check calls
372
+ server.mockService.check.mockImplementation(() => {
373
+ serviceStatuses.push('Service checked')
374
+ })
375
+
376
+ server.mockWeb.check.mockImplementation(() => {
377
+ serviceStatuses.push('Web checked')
378
+ })
379
+
380
+ // Run multiple monitoring cycles
381
+ jest.advanceTimersByTime(2000) // First cycle
382
+ jest.advanceTimersByTime(1000) // Second cycle
383
+ jest.advanceTimersByTime(1000) // Third cycle
384
+
385
+ expect(serviceStatuses).toEqual([
386
+ 'Service checked',
387
+ 'Web checked',
388
+ 'Service checked',
389
+ 'Web checked',
390
+ 'Service checked',
391
+ 'Web checked'
392
+ ])
393
+ })
394
+
395
+ test('should handle all service types in monitoring cycle', () => {
396
+ // Fast-forward to trigger multiple health check cycles
397
+ jest.advanceTimersByTime(4000) // 3 complete cycles
398
+
399
+ // Verify all service types are monitored consistently
400
+ expect(server.mockService.check).toHaveBeenCalledTimes(3)
401
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(3)
402
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(3)
403
+ expect(server.mockMail.check).toHaveBeenCalledTimes(3)
404
+ })
405
+
406
+ test('should isolate service check failures from each other', () => {
407
+ // Mock different services to fail at different times
408
+ let serviceCallCount = 0
409
+ let sslCallCount = 0
410
+
411
+ server.mockService.check.mockImplementation(() => {
412
+ serviceCallCount++
413
+ if (serviceCallCount === 2) {
414
+ throw new Error('Service check failed on second call')
415
+ }
416
+ })
417
+
418
+ server.mockSSL.check.mockImplementation(() => {
419
+ sslCallCount++
420
+ if (sslCallCount === 1) {
421
+ throw new Error('SSL check failed on first call')
422
+ }
423
+ })
424
+
425
+ // Run multiple cycles
426
+ jest.advanceTimersByTime(2000) // First cycle - SSL fails
427
+ jest.advanceTimersByTime(1000) // Second cycle - Service fails
428
+ jest.advanceTimersByTime(1000) // Third cycle - both succeed
429
+
430
+ // All services should have been called despite individual failures
431
+ expect(server.mockService.check).toHaveBeenCalledTimes(3)
432
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(3)
433
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(3)
434
+ expect(server.mockMail.check).toHaveBeenCalledTimes(3)
435
+ })
436
+
437
+ test('should maintain monitoring state across service restarts', () => {
438
+ // Start monitoring
439
+ jest.advanceTimersByTime(2000)
440
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
441
+
442
+ // Simulate service restart by resetting mock
443
+ server.mockService.check.mockClear()
444
+
445
+ // Continue monitoring
446
+ jest.advanceTimersByTime(2000)
447
+ expect(server.mockService.check).toHaveBeenCalledTimes(2)
448
+ })
449
+ })
450
+
451
+ describe('graceful shutdown', () => {
452
+ beforeEach(() => {
453
+ server = new TestServer()
454
+ })
455
+
456
+ test('should stop all services when stop() is called', () => {
457
+ server.stop()
458
+
459
+ expect(server.mockService.stopAll).toHaveBeenCalledTimes(1)
460
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(1)
461
+ })
462
+
463
+ test('should handle service stop errors gracefully', () => {
464
+ // Mock service stopAll to throw an error
465
+ server.mockService.stopAll.mockImplementation(() => {
466
+ throw new Error('Service stop failed')
467
+ })
468
+
469
+ // Should not throw when stopping services
470
+ expect(() => {
471
+ server.stop()
472
+ }).not.toThrow()
473
+
474
+ // Web service should still be stopped
475
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(1)
476
+ })
477
+
478
+ test('should stop services in correct order', () => {
479
+ const stopOrder = []
480
+
481
+ server.mockService.stopAll.mockImplementation(() => {
482
+ stopOrder.push('Service')
483
+ })
484
+
485
+ server.mockWeb.stopAll.mockImplementation(() => {
486
+ stopOrder.push('Web')
487
+ })
488
+
489
+ server.stop()
490
+
491
+ expect(stopOrder).toEqual(['Service', 'Web'])
492
+ })
493
+
494
+ test('should handle partial service stop failures', () => {
495
+ // Mock Web stopAll to fail
496
+ server.mockWeb.stopAll.mockImplementation(() => {
497
+ throw new Error('Web stop failed')
498
+ })
499
+
500
+ expect(() => {
501
+ server.stop()
502
+ }).not.toThrow()
503
+
504
+ // Service should still be stopped
505
+ expect(server.mockService.stopAll).toHaveBeenCalledTimes(1)
506
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(1)
507
+ })
508
+
509
+ test('should clear health check intervals during shutdown', () => {
510
+ // Start health checks
511
+ jest.advanceTimersByTime(2000)
512
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
513
+
514
+ // Stop the server
515
+ server.stop()
516
+
517
+ // Advance time and verify no more health checks occur
518
+ jest.advanceTimersByTime(5000)
519
+ expect(server.mockService.check).toHaveBeenCalledTimes(1) // Should remain 1
520
+ })
521
+
522
+ test('should handle shutdown when health checks are not yet started', () => {
523
+ // Stop server immediately after creation
524
+ server.stop()
525
+
526
+ // Verify shutdown methods are called
527
+ expect(server.mockService.stopAll).toHaveBeenCalledTimes(1)
528
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(1)
529
+
530
+ // Test should not throw and shutdown should complete successfully
531
+ expect(() => {
532
+ jest.advanceTimersByTime(5000)
533
+ }).not.toThrow()
534
+ })
535
+
536
+ test('should perform complete cleanup of all resources', () => {
537
+ // Start health checks
538
+ jest.advanceTimersByTime(2000)
539
+
540
+ // Verify health checks are running
541
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
542
+
543
+ // Stop server
544
+ server.stop()
545
+
546
+ // Verify all cleanup actions
547
+ expect(server.mockService.stopAll).toHaveBeenCalledTimes(1)
548
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(1)
549
+
550
+ // Verify no more health checks after cleanup
551
+ jest.advanceTimersByTime(10000)
552
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
553
+ expect(server.mockSSL.check).toHaveBeenCalledTimes(1)
554
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(1)
555
+ expect(server.mockMail.check).toHaveBeenCalledTimes(1)
556
+ })
557
+
558
+ test('should handle multiple shutdown calls gracefully', () => {
559
+ // Call stop multiple times
560
+ server.stop()
561
+ server.stop()
562
+ server.stop()
563
+
564
+ // Services should only be stopped once
565
+ expect(server.mockService.stopAll).toHaveBeenCalledTimes(3)
566
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(3)
567
+ })
568
+ })
569
+
570
+ describe('resource deallocation and cleanup', () => {
571
+ beforeEach(() => {
572
+ server = new TestServer()
573
+ })
574
+
575
+ test('should deallocate all monitoring intervals during shutdown', () => {
576
+ // Start monitoring
577
+ jest.advanceTimersByTime(2000)
578
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
579
+
580
+ // Verify interval is active
581
+ const activeTimers = jest.getTimerCount()
582
+ expect(activeTimers).toBeGreaterThan(0)
583
+
584
+ // Stop server and verify interval cleanup
585
+ server.stop()
586
+
587
+ // Advance time significantly and verify no more checks
588
+ jest.advanceTimersByTime(10000)
589
+ expect(server.mockService.check).toHaveBeenCalledTimes(1) // Should not increase
590
+ })
591
+
592
+ test('should handle cleanup when no intervals are active', () => {
593
+ // Stop server before any intervals are created
594
+ server.stop()
595
+
596
+ expect(() => {
597
+ jest.advanceTimersByTime(5000)
598
+ }).not.toThrow()
599
+
600
+ expect(server.mockService.stopAll).toHaveBeenCalledTimes(1)
601
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(1)
602
+ })
603
+
604
+ test('should perform complete resource cleanup sequence', () => {
605
+ const cleanupOrder = []
606
+
607
+ server.mockService.stopAll.mockImplementation(() => {
608
+ cleanupOrder.push('Service cleanup')
609
+ })
610
+
611
+ server.mockWeb.stopAll.mockImplementation(() => {
612
+ cleanupOrder.push('Web cleanup')
613
+ })
614
+
615
+ // Start monitoring to create resources
616
+ jest.advanceTimersByTime(2000)
617
+
618
+ // Perform cleanup
619
+ server.stop()
620
+
621
+ expect(cleanupOrder).toEqual(['Service cleanup', 'Web cleanup'])
622
+ })
623
+
624
+ test('should handle resource cleanup errors without failing', () => {
625
+ // Mock cleanup methods to throw errors
626
+ server.mockService.stopAll.mockImplementation(() => {
627
+ throw new Error('Service cleanup failed')
628
+ })
629
+
630
+ server.mockWeb.stopAll.mockImplementation(() => {
631
+ throw new Error('Web cleanup failed')
632
+ })
633
+
634
+ // Should not throw during cleanup
635
+ expect(() => {
636
+ server.stop()
637
+ }).not.toThrow()
638
+
639
+ // Verify cleanup was attempted
640
+ expect(server.mockService.stopAll).toHaveBeenCalledTimes(1)
641
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(1)
642
+ })
643
+
644
+ test('should ensure no memory leaks from monitoring intervals', () => {
645
+ // Test that stopped servers don't continue monitoring
646
+ const testServer1 = new TestServer()
647
+
648
+ // Start monitoring for first server
649
+ jest.advanceTimersByTime(2000)
650
+ const firstServerCalls = testServer1.mockService.check.mock.calls.length
651
+ expect(firstServerCalls).toBeGreaterThan(0)
652
+
653
+ // Stop and destroy first server
654
+ testServer1.destroy()
655
+
656
+ // Create second server with fresh mocks
657
+ const testServer2 = new TestServer()
658
+
659
+ // Advance time - only second server should have monitoring
660
+ jest.advanceTimersByTime(3000)
661
+
662
+ // First server should not have additional calls after stop
663
+ expect(testServer1.mockService.check).toHaveBeenCalledTimes(firstServerCalls)
664
+
665
+ // Second server should have its own monitoring calls
666
+ expect(testServer2.mockService.check.mock.calls.length).toBeGreaterThan(0)
667
+
668
+ // Clean up second server
669
+ testServer2.destroy()
670
+ })
671
+
672
+ test('should clean up service references during shutdown', () => {
673
+ // Start monitoring
674
+ jest.advanceTimersByTime(2000)
675
+
676
+ // Verify services are being monitored
677
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
678
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(1)
679
+
680
+ // Stop server
681
+ server.stop()
682
+
683
+ // Verify service cleanup was called
684
+ expect(server.mockService.stopAll).toHaveBeenCalledTimes(1)
685
+ expect(server.mockWeb.stopAll).toHaveBeenCalledTimes(1)
686
+
687
+ // Verify no further service interactions
688
+ jest.advanceTimersByTime(5000)
689
+ expect(server.mockService.check).toHaveBeenCalledTimes(1)
690
+ expect(server.mockWeb.check).toHaveBeenCalledTimes(1)
691
+ })
692
+ })
693
+
694
+ describe('configuration management', () => {
695
+ test('should update config with current process PID', () => {
696
+ const originalPid = process.pid
697
+
698
+ // Set the PID before creating the server
699
+ Object.defineProperty(process, 'pid', {
700
+ value: 54321,
701
+ writable: true
702
+ })
703
+
704
+ server = new TestServer()
705
+
706
+ expect(server.mockConfig.server.pid).toBe(54321)
707
+
708
+ // Restore original PID
709
+ Object.defineProperty(process, 'pid', {
710
+ value: originalPid,
711
+ writable: true
712
+ })
713
+ })
714
+
715
+ test('should record server startup timestamp', () => {
716
+ const mockTimestamp = 1641081600000 // 2022-01-02 00:00:00
717
+ jest.spyOn(Date, 'now').mockReturnValue(mockTimestamp)
718
+
719
+ server = new TestServer()
720
+
721
+ expect(server.mockConfig.server.started).toBe(mockTimestamp)
722
+
723
+ Date.now.mockRestore()
724
+ })
725
+
726
+ test('should handle config access errors gracefully', () => {
727
+ // Create a function that simulates the Server initialization with config error
728
+ const createServerWithConfigError = () => {
729
+ // Setup global Candy mock that throws on Config access
730
+ global.Candy = {
731
+ core: jest.fn(module => {
732
+ if (module === 'Config') {
733
+ throw new Error('Config access failed')
734
+ }
735
+ return {}
736
+ }),
737
+ server: jest.fn(() => ({}))
738
+ }
739
+
740
+ // Simulate the Server initialization process
741
+ global.Candy.core('Config').config.server.pid = process.pid
742
+ }
743
+
744
+ expect(() => {
745
+ createServerWithConfigError()
746
+ }).toThrow('Config access failed')
747
+ })
748
+
749
+ test('should preserve existing config structure', () => {
750
+ server = new TestServer()
751
+
752
+ // Add existing config data
753
+ server.mockConfig.server.existingProperty = 'existing value'
754
+ server.mockConfig.otherSection = {data: 'preserved'}
755
+
756
+ // Re-initialize to test preservation
757
+ server.init()
758
+
759
+ expect(server.mockConfig.server.existingProperty).toBe('existing value')
760
+ expect(server.mockConfig.otherSection).toEqual({data: 'preserved'})
761
+ expect(server.mockConfig.server.pid).toBe(process.pid)
762
+ expect(server.mockConfig.server.started).toBeDefined()
763
+ })
764
+ })
765
+ })