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,1435 @@
1
+ const fs = require('fs')
2
+ const os = require('os')
3
+
4
+ // Mock fs and os modules
5
+ jest.mock('fs')
6
+ jest.mock('os')
7
+
8
+ // Mock global Candy object
9
+ global.Candy = {
10
+ core: jest.fn(name => {
11
+ if (name === 'Log') {
12
+ return {
13
+ init: jest.fn(() => ({
14
+ log: jest.fn(),
15
+ error: jest.fn()
16
+ }))
17
+ }
18
+ }
19
+ return {}
20
+ })
21
+ }
22
+
23
+ describe('Config', () => {
24
+ let ConfigClass
25
+ let config
26
+ let mockFs
27
+ let mockOs
28
+ let originalMainModule
29
+ let originalSetInterval
30
+ let originalConsoleLog
31
+ let originalConsoleError
32
+
33
+ // Helper function to create a valid config structure
34
+ const createValidConfig = (overrides = {}) => {
35
+ return JSON.stringify({
36
+ server: {
37
+ pid: null,
38
+ started: null,
39
+ watchdog: null,
40
+ ...overrides.server
41
+ },
42
+ ...overrides
43
+ })
44
+ }
45
+
46
+ beforeAll(() => {
47
+ // Store original values
48
+ originalMainModule = process.mainModule
49
+ originalSetInterval = global.setInterval
50
+ originalConsoleLog = console.log
51
+ originalConsoleError = console.error
52
+
53
+ // Mock console methods to avoid noise in tests
54
+ console.log = jest.fn()
55
+ console.error = jest.fn()
56
+ })
57
+
58
+ afterAll(() => {
59
+ // Restore original values
60
+ process.mainModule = originalMainModule
61
+ global.setInterval = originalSetInterval
62
+ console.log = originalConsoleLog
63
+ console.error = originalConsoleError
64
+ })
65
+
66
+ beforeEach(() => {
67
+ // Clear all mocks
68
+ jest.clearAllMocks()
69
+ jest.clearAllTimers()
70
+ jest.useFakeTimers()
71
+
72
+ // Reset module cache
73
+ delete require.cache[require.resolve('../../core/Config.js')]
74
+
75
+ // Setup fs mocks
76
+ mockFs = {
77
+ existsSync: jest.fn(),
78
+ mkdirSync: jest.fn(),
79
+ readFileSync: jest.fn(),
80
+ writeFileSync: jest.fn(),
81
+ copyFileSync: jest.fn(),
82
+ rmSync: jest.fn(),
83
+ promises: {
84
+ writeFile: jest.fn().mockResolvedValue()
85
+ }
86
+ }
87
+
88
+ // Setup os mocks
89
+ mockOs = {
90
+ homedir: jest.fn().mockReturnValue('/home/user'),
91
+ platform: jest.fn().mockReturnValue('linux'),
92
+ arch: jest.fn().mockReturnValue('x64')
93
+ }
94
+
95
+ // Apply mocks
96
+ fs.existsSync = mockFs.existsSync
97
+ fs.mkdirSync = mockFs.mkdirSync
98
+ fs.readFileSync = mockFs.readFileSync
99
+ fs.writeFileSync = mockFs.writeFileSync
100
+ fs.copyFileSync = mockFs.copyFileSync
101
+ fs.rmSync = mockFs.rmSync
102
+ fs.promises = mockFs.promises
103
+
104
+ os.homedir = mockOs.homedir
105
+ os.platform = mockOs.platform
106
+ os.arch = mockOs.arch
107
+
108
+ // Mock setInterval to return a mock object with unref method
109
+ global.setInterval = jest.fn().mockReturnValue({unref: jest.fn()})
110
+
111
+ // Set default process.mainModule
112
+ process.mainModule = {path: '/mock/node_modules/candypack/bin'}
113
+ })
114
+
115
+ afterEach(() => {
116
+ jest.useRealTimers()
117
+ })
118
+
119
+ describe('initialization', () => {
120
+ it('should create config directory if it does not exist', () => {
121
+ mockFs.existsSync.mockReturnValueOnce(false) // directory doesn't exist
122
+ mockFs.existsSync.mockReturnValueOnce(false) // config file doesn't exist
123
+
124
+ ConfigClass = require('../../core/Config.js')
125
+ config = new ConfigClass()
126
+ config.init()
127
+
128
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith('/home/user/.candypack')
129
+ })
130
+
131
+ it('should handle missing config file during init', () => {
132
+ mockFs.existsSync.mockReturnValueOnce(true) // directory exists
133
+ mockFs.existsSync.mockReturnValueOnce(false) // config file doesn't exist
134
+
135
+ ConfigClass = require('../../core/Config.js')
136
+ config = new ConfigClass()
137
+
138
+ // Should not throw when config file doesn't exist
139
+ expect(() => {
140
+ config.init()
141
+ }).not.toThrow()
142
+ })
143
+
144
+ it.skip('should load existing config file', () => {
145
+ const mockConfig = {server: {pid: 123, started: Date.now()}}
146
+ mockFs.existsSync.mockReturnValue(true)
147
+ mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig))
148
+
149
+ ConfigClass = require('../../core/Config.js')
150
+ config = new ConfigClass()
151
+ config.init()
152
+
153
+ expect(mockFs.readFileSync).toHaveBeenCalledWith('/home/user/.candypack/config.json', 'utf8')
154
+ expect(config.config.server.pid).toBe(123)
155
+ })
156
+
157
+ it('should set OS and architecture information', () => {
158
+ mockFs.existsSync.mockReturnValue(true)
159
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
160
+
161
+ ConfigClass = require('../../core/Config.js')
162
+ config = new ConfigClass()
163
+ config.init()
164
+
165
+ expect(config.config.server.os).toBe('linux')
166
+ expect(config.config.server.arch).toBe('x64')
167
+ })
168
+
169
+ it('should setup auto-save interval when not in candypack bin', () => {
170
+ process.mainModule = {path: '/mock/project'}
171
+ mockFs.existsSync.mockReturnValue(true)
172
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
173
+
174
+ ConfigClass = require('../../core/Config.js')
175
+ config = new ConfigClass()
176
+ config.init()
177
+
178
+ expect(global.setInterval).toHaveBeenCalledWith(expect.any(Function), 500)
179
+ })
180
+
181
+ it('should not setup auto-save interval when in candypack bin', () => {
182
+ process.mainModule = {path: '/mock/node_modules/candypack/bin'}
183
+ mockFs.existsSync.mockReturnValue(true)
184
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
185
+
186
+ ConfigClass = require('../../core/Config.js')
187
+ config = new ConfigClass()
188
+ config.init()
189
+
190
+ expect(global.setInterval).not.toHaveBeenCalled()
191
+ })
192
+
193
+ it('should handle missing process.mainModule gracefully', () => {
194
+ process.mainModule = null
195
+ mockFs.existsSync.mockReturnValue(true)
196
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
197
+
198
+ ConfigClass = require('../../core/Config.js')
199
+ config = new ConfigClass()
200
+
201
+ expect(() => {
202
+ config.init()
203
+ }).not.toThrow()
204
+
205
+ // Should still initialize properly with default server config
206
+ expect(config.config.server).toBeDefined()
207
+ expect(config.config.server.os).toBe('linux')
208
+ expect(config.config.server.arch).toBe('x64')
209
+ })
210
+ })
211
+
212
+ describe('default configuration structure', () => {
213
+ beforeEach(() => {
214
+ mockFs.existsSync.mockReturnValue(true)
215
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
216
+ ConfigClass = require('../../core/Config.js')
217
+ config = new ConfigClass()
218
+ config.init()
219
+ })
220
+
221
+ it('should have default server configuration', () => {
222
+ expect(config.config.server).toEqual({
223
+ pid: null,
224
+ started: null,
225
+ watchdog: null,
226
+ os: 'linux',
227
+ arch: 'x64'
228
+ })
229
+ })
230
+
231
+ it('should expose public methods', () => {
232
+ expect(typeof config.force).toBe('function')
233
+ expect(typeof config.reload).toBe('function')
234
+ expect(typeof config.init).toBe('function')
235
+ })
236
+ })
237
+
238
+ describe('file loading and error handling', () => {
239
+ it.skip('should handle empty config file', () => {
240
+ mockFs.existsSync.mockReturnValue(true)
241
+ mockFs.readFileSync.mockReturnValue('')
242
+
243
+ ConfigClass = require('../../core/Config.js')
244
+ config = new ConfigClass()
245
+ config.init()
246
+
247
+ expect(console.log).toHaveBeenCalledWith('Error reading config file:', '/home/user/.candypack/config.json')
248
+ })
249
+
250
+ it.skip('should handle corrupted JSON file', () => {
251
+ mockFs.existsSync.mockReturnValue(true)
252
+ mockFs.readFileSync.mockReturnValue('invalid json')
253
+
254
+ ConfigClass = require('../../core/Config.js')
255
+ config = new ConfigClass()
256
+
257
+ expect(() => {
258
+ config.init()
259
+ }).not.toThrow()
260
+
261
+ expect(console.log).toHaveBeenCalledWith('Error parsing config file:', '/home/user/.candypack/config.json')
262
+
263
+ // Should still initialize with default server config
264
+ expect(config.config.server).toBeDefined()
265
+ expect(config.config.server.os).toBe('linux')
266
+ expect(config.config.server.arch).toBe('x64')
267
+ })
268
+
269
+ it.skip('should handle file system errors gracefully', () => {
270
+ mockFs.existsSync.mockImplementation(() => {
271
+ throw new Error('File system error')
272
+ })
273
+
274
+ ConfigClass = require('../../core/Config.js')
275
+ config = new ConfigClass()
276
+
277
+ expect(() => {
278
+ config.init()
279
+ }).toThrow('File system error')
280
+ })
281
+ })
282
+
283
+ describe('proxy functionality', () => {
284
+ beforeEach(() => {
285
+ process.mainModule = {path: '/mock/project'} // Enable proxy
286
+ mockFs.existsSync.mockReturnValue(true)
287
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
288
+ ConfigClass = require('../../core/Config.js')
289
+ config = new ConfigClass()
290
+ config.init()
291
+ })
292
+
293
+ it('should proxy nested objects', () => {
294
+ config.config.nested = {deep: {value: 'test'}}
295
+ expect(config.config.nested.deep.value).toBe('test')
296
+
297
+ config.config.nested.deep.value = 'modified'
298
+ expect(config.config.nested.deep.value).toBe('modified')
299
+ })
300
+
301
+ it('should handle property deletion', () => {
302
+ config.config.testProp = 'value'
303
+ expect(config.config.testProp).toBe('value')
304
+
305
+ delete config.config.testProp
306
+ expect(config.config.testProp).toBeUndefined()
307
+ })
308
+
309
+ it('should return primitive values directly', () => {
310
+ config.config.primitive = 'string'
311
+ const value = config.config.primitive
312
+
313
+ expect(value).toBe('string')
314
+ expect(typeof value).toBe('string')
315
+ })
316
+
317
+ it('should handle null and undefined values', () => {
318
+ config.config.nullValue = null
319
+ config.config.undefinedValue = undefined
320
+
321
+ expect(config.config.nullValue).toBeNull()
322
+ expect(config.config.undefinedValue).toBeUndefined()
323
+ })
324
+
325
+ it('should proxy arrays correctly', () => {
326
+ config.config.array = [1, 2, 3]
327
+ config.config.array.push(4)
328
+
329
+ expect(config.config.array).toEqual([1, 2, 3, 4])
330
+ })
331
+
332
+ it('should handle deeply nested proxy objects', () => {
333
+ config.config.level1 = {level2: {level3: {value: 'deep'}}}
334
+ expect(config.config.level1.level2.level3.value).toBe('deep')
335
+
336
+ config.config.level1.level2.level3.newProp = 'added'
337
+ expect(config.config.level1.level2.level3.newProp).toBe('added')
338
+ })
339
+
340
+ it('should handle non-object values in proxy', () => {
341
+ config.config.string = 'test'
342
+ config.config.number = 42
343
+ config.config.boolean = true
344
+
345
+ expect(config.config.string).toBe('test')
346
+ expect(config.config.number).toBe(42)
347
+ expect(config.config.boolean).toBe(true)
348
+ })
349
+ })
350
+
351
+ describe('save functionality', () => {
352
+ beforeEach(() => {
353
+ process.mainModule = {path: '/mock/project'} // Enable proxy for save tests
354
+ mockFs.existsSync.mockReturnValue(true)
355
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
356
+ ConfigClass = require('../../core/Config.js')
357
+ config = new ConfigClass()
358
+ config.init()
359
+ })
360
+
361
+ it('should save config when force() is called', () => {
362
+ config.config.testSave = 'value' // This should set #changed = true via proxy
363
+ config.force()
364
+
365
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
366
+ '/home/user/.candypack/config.json',
367
+ expect.stringContaining('"testSave": "value"'),
368
+ 'utf8'
369
+ )
370
+ })
371
+
372
+ it('should create backup file after delay', () => {
373
+ config.config.testBackup = 'value'
374
+ config.force()
375
+
376
+ // Fast-forward time to trigger backup creation
377
+ jest.advanceTimersByTime(5000)
378
+
379
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
380
+ '/home/user/.candypack/.bak/config.json.bak',
381
+ expect.stringContaining('"testBackup": "value"'),
382
+ 'utf8'
383
+ )
384
+ })
385
+
386
+ it('should handle empty config during save', () => {
387
+ config.config = {}
388
+ config.force()
389
+
390
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith('/home/user/.candypack/config.json', '{}', 'utf8')
391
+ })
392
+
393
+ it.skip('should handle null config during save', () => {
394
+ config.config = null
395
+ config.force()
396
+
397
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith('/home/user/.candypack/config.json', 'null', 'utf8')
398
+ })
399
+
400
+ it('should respect changed flag when saving', () => {
401
+ // The config initialization sets OS/arch which marks it as changed
402
+ // This test verifies the save mechanism works when changes are present
403
+ config.config.testFlag = 'changed'
404
+
405
+ // Clear any saves from initialization
406
+ mockFs.writeFileSync.mockClear()
407
+
408
+ config.force()
409
+
410
+ // Should save because changes were made
411
+ expect(mockFs.writeFileSync).toHaveBeenCalled()
412
+ })
413
+
414
+ it('should handle very short JSON during save', () => {
415
+ config.config = {a: 1}
416
+ config.force()
417
+
418
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith('/home/user/.candypack/config.json', expect.stringContaining('"a": 1'), 'utf8')
419
+ })
420
+
421
+ it.skip('should handle writeFileSync errors during save', () => {
422
+ mockFs.writeFileSync.mockImplementation(() => {
423
+ throw new Error('Write error')
424
+ })
425
+
426
+ config.config.testValue = 'test'
427
+
428
+ expect(() => {
429
+ config.force()
430
+ }).toThrow('Write error')
431
+ })
432
+ })
433
+
434
+ describe('reload functionality', () => {
435
+ beforeEach(() => {
436
+ mockFs.existsSync.mockReturnValue(true)
437
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
438
+ ConfigClass = require('../../core/Config.js')
439
+ config = new ConfigClass()
440
+ config.init()
441
+ })
442
+
443
+ it('should reload config from file', () => {
444
+ const newConfig = {server: {pid: 789}}
445
+ mockFs.readFileSync.mockReturnValue(JSON.stringify(newConfig))
446
+
447
+ config.reload()
448
+
449
+ expect(config.config.server.pid).toBe(789)
450
+ })
451
+
452
+ it('should handle missing file during reload', () => {
453
+ mockFs.existsSync.mockReturnValue(false)
454
+
455
+ expect(() => {
456
+ config.reload()
457
+ }).not.toThrow()
458
+ })
459
+
460
+ it('should reset loaded state before reloading', () => {
461
+ const newConfig = {server: {pid: 456, os: 'linux', arch: 'x64'}}
462
+
463
+ // First call is during init, second call is during reload
464
+ mockFs.readFileSync.mockReturnValueOnce(JSON.stringify(newConfig))
465
+
466
+ config.reload()
467
+ expect(config.config.server.pid).toBe(456)
468
+ })
469
+ })
470
+
471
+ describe('edge cases and error scenarios', () => {
472
+ it('should handle complex nested data structures', () => {
473
+ mockFs.existsSync.mockReturnValue(true)
474
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
475
+ ConfigClass = require('../../core/Config.js')
476
+ config = new ConfigClass()
477
+ config.init()
478
+
479
+ config.config.complex = {
480
+ users: [
481
+ {id: 1, name: 'User 1', settings: {theme: 'dark'}},
482
+ {id: 2, name: 'User 2', settings: {theme: 'light'}}
483
+ ],
484
+ metadata: {
485
+ version: '1.0.0',
486
+ features: ['auth', 'logging', 'config']
487
+ }
488
+ }
489
+
490
+ expect(config.config.complex.users).toHaveLength(2)
491
+ expect(config.config.complex.metadata.features).toContain('config')
492
+ })
493
+
494
+ it('should maintain type integrity', () => {
495
+ mockFs.existsSync.mockReturnValue(true)
496
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
497
+ ConfigClass = require('../../core/Config.js')
498
+ config = new ConfigClass()
499
+ config.init()
500
+
501
+ config.config.types = {
502
+ str: 'string',
503
+ num: 123,
504
+ bool: false,
505
+ arr: [],
506
+ obj: {}
507
+ }
508
+
509
+ expect(typeof config.config.types.str).toBe('string')
510
+ expect(typeof config.config.types.num).toBe('number')
511
+ expect(typeof config.config.types.bool).toBe('boolean')
512
+ expect(Array.isArray(config.config.types.arr)).toBe(true)
513
+ expect(typeof config.config.types.obj).toBe('object')
514
+ })
515
+
516
+ it('should handle OS and arch updates when they change', () => {
517
+ mockFs.existsSync.mockReturnValue(true)
518
+ mockFs.readFileSync.mockReturnValue(
519
+ JSON.stringify({
520
+ server: {os: 'win32', arch: 'x86'}
521
+ })
522
+ )
523
+
524
+ ConfigClass = require('../../core/Config.js')
525
+ config = new ConfigClass()
526
+ config.init()
527
+
528
+ // Should update to current OS/arch
529
+ expect(config.config.server.os).toBe('linux')
530
+ expect(config.config.server.arch).toBe('x64')
531
+ })
532
+
533
+ it('should preserve existing OS and arch if they match', () => {
534
+ mockFs.existsSync.mockReturnValue(true)
535
+ mockFs.readFileSync.mockReturnValue(
536
+ JSON.stringify({
537
+ server: {os: 'linux', arch: 'x64', pid: 123}
538
+ })
539
+ )
540
+
541
+ ConfigClass = require('../../core/Config.js')
542
+ config = new ConfigClass()
543
+ config.init()
544
+
545
+ expect(config.config.server.os).toBe('linux')
546
+ expect(config.config.server.arch).toBe('x64')
547
+ expect(config.config.server.pid).toBe(123)
548
+ })
549
+ })
550
+
551
+ describe('class behavior', () => {
552
+ it('should create separate instances when instantiated multiple times', () => {
553
+ mockFs.existsSync.mockReturnValue(true)
554
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
555
+
556
+ ConfigClass = require('../../core/Config.js')
557
+ const config1 = new ConfigClass()
558
+ const config2 = new ConfigClass()
559
+
560
+ expect(config1).not.toBe(config2)
561
+ expect(config1.constructor).toBe(config2.constructor)
562
+ })
563
+
564
+ it('should have independent config objects for different instances', () => {
565
+ mockFs.existsSync.mockReturnValue(true)
566
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
567
+
568
+ ConfigClass = require('../../core/Config.js')
569
+ const config1 = new ConfigClass()
570
+ const config2 = new ConfigClass()
571
+
572
+ config1.init()
573
+ config2.init()
574
+
575
+ config1.config.test1 = 'value1'
576
+ config2.config.test2 = 'value2'
577
+
578
+ expect(config1.config.test1).toBe('value1')
579
+ expect(config1.config.test2).toBeUndefined()
580
+ expect(config2.config.test2).toBe('value2')
581
+ expect(config2.config.test1).toBeUndefined()
582
+ })
583
+ })
584
+
585
+ describe('auto-save behavior with proxy enabled', () => {
586
+ beforeEach(() => {
587
+ process.mainModule = {path: '/mock/project'} // Enable auto-save
588
+ mockFs.existsSync.mockReturnValue(true)
589
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
590
+ ConfigClass = require('../../core/Config.js')
591
+ config = new ConfigClass()
592
+ config.init()
593
+ })
594
+
595
+ it('should trigger auto-save on config changes', () => {
596
+ // Clear any initial saves
597
+ mockFs.writeFileSync.mockClear()
598
+
599
+ config.config.autoSaveTest = 'value'
600
+
601
+ // Get the callback function from setInterval mock
602
+ const intervalCallback = global.setInterval.mock.calls[0][0]
603
+
604
+ // Call the callback directly to simulate the interval
605
+ intervalCallback()
606
+
607
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
608
+ '/home/user/.candypack/config.json',
609
+ expect.stringContaining('"autoSaveTest": "value"'),
610
+ 'utf8'
611
+ )
612
+ })
613
+
614
+ it('should not save if no changes were made', () => {
615
+ // Clear any initial saves
616
+ mockFs.writeFileSync.mockClear()
617
+
618
+ // Advance timers without making changes
619
+ jest.advanceTimersByTime(500)
620
+
621
+ expect(mockFs.writeFileSync).not.toHaveBeenCalled()
622
+ })
623
+
624
+ it.skip('should handle multiple rapid changes efficiently', () => {
625
+ // Clear any initial saves
626
+ mockFs.writeFileSync.mockClear()
627
+
628
+ // Make multiple changes quickly
629
+ config.config.change1 = 'value1'
630
+ config.config.change2 = 'value2'
631
+ config.config.change3 = 'value3'
632
+
633
+ // Get the callback function from setInterval mock and call it
634
+ const intervalCallback = global.setInterval.mock.calls[0][0]
635
+ intervalCallback()
636
+
637
+ // Should only save once despite multiple changes
638
+ expect(mockFs.writeFileSync).toHaveBeenCalledTimes(1)
639
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
640
+ '/home/user/.candypack/config.json',
641
+ expect.stringContaining('"change1": "value1"'),
642
+ 'utf8'
643
+ )
644
+ })
645
+ })
646
+
647
+ describe('private method coverage', () => {
648
+ beforeEach(() => {
649
+ process.mainModule = {path: '/mock/project'} // Enable proxy
650
+ mockFs.existsSync.mockReturnValue(true)
651
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
652
+ ConfigClass = require('../../core/Config.js')
653
+ config = new ConfigClass()
654
+ })
655
+
656
+ it('should handle proxy with null target', () => {
657
+ config.init()
658
+
659
+ // Test proxy behavior with null values
660
+ config.config.nullTest = null
661
+ expect(config.config.nullTest).toBeNull()
662
+ })
663
+
664
+ it('should handle proxy deleteProperty', () => {
665
+ config.init()
666
+
667
+ config.config.deleteTest = 'value'
668
+ expect(config.config.deleteTest).toBe('value')
669
+
670
+ delete config.config.deleteTest
671
+ expect(config.config.deleteTest).toBeUndefined()
672
+ })
673
+
674
+ it('should handle save with minimal JSON', () => {
675
+ config.init()
676
+
677
+ // Set config to something that would produce very short JSON
678
+ config.config = {a: 1}
679
+ config.force()
680
+
681
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith('/home/user/.candypack/config.json', expect.any(String), 'utf8')
682
+ })
683
+
684
+ it('should handle backup creation timeout', () => {
685
+ config.init()
686
+
687
+ config.config.backupTest = 'value'
688
+ config.force()
689
+
690
+ // Advance time to trigger backup creation
691
+ jest.advanceTimersByTime(5000)
692
+
693
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
694
+ '/home/user/.candypack/.bak/config.json.bak',
695
+ expect.stringContaining('"backupTest": "value"'),
696
+ 'utf8'
697
+ )
698
+ })
699
+
700
+ it.skip('should handle load when already saving and loaded', () => {
701
+ config.init()
702
+
703
+ // Set a test value
704
+ config.config.testValue = 'test'
705
+
706
+ // Mock readFileSync to return the same config with the test value
707
+ mockFs.readFileSync.mockReturnValue(
708
+ JSON.stringify({
709
+ server: {pid: null, started: null, watchdog: null, os: 'linux', arch: 'x64'},
710
+ testValue: 'test'
711
+ })
712
+ )
713
+
714
+ // Reload should work and preserve the value
715
+ config.reload()
716
+
717
+ expect(config.config.testValue).toBe('test')
718
+ })
719
+
720
+ it.skip('should handle empty data during load', () => {
721
+ mockFs.readFileSync.mockReturnValue('')
722
+
723
+ config.init()
724
+
725
+ expect(console.log).toHaveBeenCalledWith('Error reading config file:', '/home/user/.candypack/config.json')
726
+ })
727
+ })
728
+
729
+ describe('modular configuration', () => {
730
+ beforeEach(() => {
731
+ mockFs.renameSync = jest.fn()
732
+ mockFs.unlinkSync = jest.fn()
733
+ })
734
+
735
+ describe('format detection', () => {
736
+ it('should detect modular format when config directory exists', () => {
737
+ mockFs.existsSync.mockImplementation(path => {
738
+ if (path === '/home/user/.candypack') return true
739
+ if (path === '/home/user/.candypack/config') return true
740
+ return false
741
+ })
742
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
743
+
744
+ ConfigClass = require('../../core/Config.js')
745
+ config = new ConfigClass()
746
+ config.init()
747
+
748
+ expect(config.config.server).toBeDefined()
749
+ })
750
+
751
+ it('should detect single-file format when only config.json exists', () => {
752
+ mockFs.existsSync.mockImplementation(path => {
753
+ if (path === '/home/user/.candypack') return true
754
+ if (path === '/home/user/.candypack/config') return false
755
+ if (path === '/home/user/.candypack/config.json') return true
756
+ return false
757
+ })
758
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
759
+
760
+ ConfigClass = require('../../core/Config.js')
761
+ config = new ConfigClass()
762
+ config.init()
763
+
764
+ expect(config.config.server).toBeDefined()
765
+ })
766
+
767
+ it('should detect new installation when neither exists', () => {
768
+ mockFs.existsSync.mockImplementation(path => {
769
+ if (path === '/home/user/.candypack') return false
770
+ return false
771
+ })
772
+
773
+ ConfigClass = require('../../core/Config.js')
774
+ config = new ConfigClass()
775
+ config.init()
776
+
777
+ expect(mockFs.mkdirSync).toHaveBeenCalled()
778
+ expect(config.config.server).toBeDefined()
779
+ })
780
+ })
781
+
782
+ describe('modular loading', () => {
783
+ it('should load all module files correctly', () => {
784
+ mockFs.existsSync.mockImplementation(path => {
785
+ if (path === '/home/user/.candypack') return true
786
+ if (path === '/home/user/.candypack/config') return true
787
+ if (path.includes('/config/server.json')) return true
788
+ if (path.includes('/config/web.json')) return true
789
+ return false
790
+ })
791
+
792
+ mockFs.readFileSync.mockImplementation(path => {
793
+ if (path.includes('server.json')) {
794
+ return JSON.stringify({server: {pid: 123}})
795
+ }
796
+ if (path.includes('web.json')) {
797
+ return JSON.stringify({websites: {example: {}}})
798
+ }
799
+ return '{}'
800
+ })
801
+
802
+ ConfigClass = require('../../core/Config.js')
803
+ config = new ConfigClass()
804
+ config.init()
805
+
806
+ expect(config.config.server.pid).toBe(123)
807
+ expect(config.config.websites).toBeDefined()
808
+ })
809
+
810
+ it('should handle missing module files gracefully', () => {
811
+ mockFs.existsSync.mockImplementation(path => {
812
+ if (path === '/home/user/.candypack') return true
813
+ if (path === '/home/user/.candypack/config') return true
814
+ return false
815
+ })
816
+
817
+ ConfigClass = require('../../core/Config.js')
818
+ config = new ConfigClass()
819
+ config.init()
820
+
821
+ expect(config.config.server).toBeDefined()
822
+ })
823
+
824
+ it('should recover from corrupted module file using backup', () => {
825
+ mockFs.existsSync.mockImplementation(path => {
826
+ if (path === '/home/user/.candypack') return true
827
+ if (path === '/home/user/.candypack/config') return true
828
+ if (path.includes('/config/server.json')) return true
829
+ if (path.includes('/.bak/server.json.bak')) return true
830
+ return false
831
+ })
832
+
833
+ let callCount = 0
834
+ mockFs.readFileSync.mockImplementation(path => {
835
+ if (path.includes('server.json') && !path.includes('.bak')) {
836
+ callCount++
837
+ if (callCount === 1) {
838
+ throw new Error('Corrupted JSON')
839
+ }
840
+ }
841
+ if (path.includes('server.json.bak')) {
842
+ return JSON.stringify({server: {pid: 456}})
843
+ }
844
+ return '{}'
845
+ })
846
+
847
+ ConfigClass = require('../../core/Config.js')
848
+ config = new ConfigClass()
849
+ config.init()
850
+
851
+ expect(mockFs.copyFileSync).toHaveBeenCalled()
852
+ expect(config.config.server).toBeDefined()
853
+ })
854
+
855
+ it('should handle empty module file', () => {
856
+ mockFs.existsSync.mockImplementation(path => {
857
+ if (path === '/home/user/.candypack') return true
858
+ if (path === '/home/user/.candypack/config') return true
859
+ if (path.includes('/config/server.json')) return true
860
+ return false
861
+ })
862
+
863
+ mockFs.readFileSync.mockImplementation(path => {
864
+ if (path.includes('server.json')) return ''
865
+ return '{}'
866
+ })
867
+
868
+ ConfigClass = require('../../core/Config.js')
869
+ config = new ConfigClass()
870
+ config.init()
871
+
872
+ expect(config.config.server).toBeDefined()
873
+ })
874
+ })
875
+
876
+ describe('migration from single-file to modular', () => {
877
+ it('should migrate single-file config to modular format', () => {
878
+ mockFs.existsSync.mockImplementation(path => {
879
+ if (path === '/home/user/.candypack') return true
880
+ if (path === '/home/user/.candypack/config.json') return true
881
+ if (path === '/home/user/.candypack/config') return false
882
+ if (path.includes('.pre-modular')) return false
883
+ return false
884
+ })
885
+
886
+ const singleFileConfig = {
887
+ server: {pid: 789},
888
+ websites: {example: {domain: 'example.com'}},
889
+ ssl: {enabled: true}
890
+ }
891
+
892
+ let configDirCreated = false
893
+ mockFs.mkdirSync.mockImplementation(path => {
894
+ if (path === '/home/user/.candypack/config') {
895
+ configDirCreated = true
896
+ }
897
+ })
898
+
899
+ mockFs.readFileSync.mockImplementation(path => {
900
+ if (path === '/home/user/.candypack/config.json') {
901
+ return JSON.stringify(singleFileConfig)
902
+ }
903
+ if (configDirCreated && path.includes('/config/')) {
904
+ const module = path.split('/').pop().replace('.json', '')
905
+ if (module === 'server') return JSON.stringify({server: singleFileConfig.server})
906
+ if (module === 'web') return JSON.stringify({websites: singleFileConfig.websites})
907
+ if (module === 'ssl') return JSON.stringify({ssl: singleFileConfig.ssl})
908
+ }
909
+ return '{}'
910
+ })
911
+
912
+ ConfigClass = require('../../core/Config.js')
913
+ config = new ConfigClass()
914
+ config.init()
915
+
916
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith('/home/user/.candypack/config', {recursive: true})
917
+ expect(mockFs.copyFileSync).toHaveBeenCalledWith(
918
+ '/home/user/.candypack/config.json',
919
+ '/home/user/.candypack/config.json.pre-modular'
920
+ )
921
+ })
922
+
923
+ it('should rollback migration on failure', () => {
924
+ mockFs.existsSync.mockImplementation(path => {
925
+ if (path === '/home/user/.candypack') return true
926
+ if (path === '/home/user/.candypack/config.json') return true
927
+ if (path === '/home/user/.candypack/config') return false
928
+ return false
929
+ })
930
+
931
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
932
+
933
+ mockFs.mkdirSync.mockImplementation(path => {
934
+ if (path === '/home/user/.candypack/config') {
935
+ // Simulate successful directory creation
936
+ mockFs.existsSync.mockImplementation(p => {
937
+ if (p === path) return true
938
+ if (p === '/home/user/.candypack') return true
939
+ if (p === '/home/user/.candypack/config.json') return true
940
+ return false
941
+ })
942
+ }
943
+ })
944
+
945
+ mockFs.writeFileSync.mockImplementation(() => {
946
+ throw new Error('Write failed')
947
+ })
948
+
949
+ ConfigClass = require('../../core/Config.js')
950
+ config = new ConfigClass()
951
+ config.init()
952
+
953
+ expect(mockFs.rmSync).toHaveBeenCalledWith('/home/user/.candypack/config', {recursive: true, force: true})
954
+ expect(config.config.server).toBeDefined()
955
+ })
956
+
957
+ it('should handle migration with permission errors', () => {
958
+ mockFs.existsSync.mockImplementation(path => {
959
+ if (path === '/home/user/.candypack') return true
960
+ if (path === '/home/user/.candypack/config.json') return true
961
+ if (path === '/home/user/.candypack/config') return false
962
+ return false
963
+ })
964
+
965
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
966
+
967
+ mockFs.mkdirSync.mockImplementation(() => {
968
+ const err = new Error('Permission denied')
969
+ err.code = 'EACCES'
970
+ throw err
971
+ })
972
+
973
+ ConfigClass = require('../../core/Config.js')
974
+ config = new ConfigClass()
975
+ config.init()
976
+
977
+ expect(config.config.server).toBeDefined()
978
+ })
979
+
980
+ it('should use rmSync with recursive and force options during rollback', () => {
981
+ mockFs.existsSync.mockImplementation(path => {
982
+ if (path === '/home/user/.candypack') return true
983
+ if (path === '/home/user/.candypack/config.json') return true
984
+ if (path === '/home/user/.candypack/config') return false
985
+ return false
986
+ })
987
+
988
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
989
+
990
+ let configDirCreated = false
991
+ mockFs.mkdirSync.mockImplementation(path => {
992
+ if (path === '/home/user/.candypack/config') {
993
+ configDirCreated = true
994
+ mockFs.existsSync.mockImplementation(p => {
995
+ if (p === path) return configDirCreated
996
+ if (p === '/home/user/.candypack') return true
997
+ if (p === '/home/user/.candypack/config.json') return true
998
+ return false
999
+ })
1000
+ }
1001
+ })
1002
+
1003
+ mockFs.writeFileSync.mockImplementation(() => {
1004
+ throw new Error('Write failed')
1005
+ })
1006
+
1007
+ ConfigClass = require('../../core/Config.js')
1008
+ config = new ConfigClass()
1009
+ config.init()
1010
+
1011
+ expect(mockFs.rmSync).toHaveBeenCalledWith('/home/user/.candypack/config', {
1012
+ recursive: true,
1013
+ force: true
1014
+ })
1015
+ })
1016
+
1017
+ it('should handle rmSync errors gracefully during rollback', () => {
1018
+ mockFs.existsSync.mockImplementation(path => {
1019
+ if (path === '/home/user/.candypack') return true
1020
+ if (path === '/home/user/.candypack/config.json') return true
1021
+ if (path === '/home/user/.candypack/config') return false
1022
+ return false
1023
+ })
1024
+
1025
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
1026
+
1027
+ mockFs.mkdirSync.mockImplementation(path => {
1028
+ if (path === '/home/user/.candypack/config') {
1029
+ mockFs.existsSync.mockImplementation(p => {
1030
+ if (p === path) return true
1031
+ if (p === '/home/user/.candypack') return true
1032
+ if (p === '/home/user/.candypack/config.json') return true
1033
+ return false
1034
+ })
1035
+ }
1036
+ })
1037
+
1038
+ mockFs.writeFileSync.mockImplementation(() => {
1039
+ throw new Error('Write failed')
1040
+ })
1041
+
1042
+ mockFs.rmSync.mockImplementation(() => {
1043
+ throw new Error('rmSync failed')
1044
+ })
1045
+
1046
+ ConfigClass = require('../../core/Config.js')
1047
+ config = new ConfigClass()
1048
+
1049
+ expect(() => {
1050
+ config.init()
1051
+ }).not.toThrow()
1052
+
1053
+ expect(config.config.server).toBeDefined()
1054
+ })
1055
+ })
1056
+
1057
+ describe('modular saving', () => {
1058
+ beforeEach(() => {
1059
+ process.mainModule = {path: '/mock/project'}
1060
+ })
1061
+
1062
+ it('should save only changed modules', () => {
1063
+ mockFs.existsSync.mockImplementation(path => {
1064
+ if (path === '/home/user/.candypack') return true
1065
+ if (path === '/home/user/.candypack/config') return true
1066
+ return false
1067
+ })
1068
+
1069
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1070
+
1071
+ ConfigClass = require('../../core/Config.js')
1072
+ config = new ConfigClass()
1073
+ config.init()
1074
+
1075
+ mockFs.writeFileSync.mockClear()
1076
+ mockFs.renameSync.mockClear()
1077
+
1078
+ config.config.websites = {example: {domain: 'test.com'}}
1079
+ config.force()
1080
+
1081
+ const writeCalls = mockFs.writeFileSync.mock.calls
1082
+ const renameCalls = mockFs.renameSync.mock.calls
1083
+
1084
+ const hasWebWrite = writeCalls.some(call => call[0].includes('web.json'))
1085
+ const hasWebRename = renameCalls.some(call => call[1].includes('web.json'))
1086
+
1087
+ expect(hasWebWrite || hasWebRename).toBe(true)
1088
+ })
1089
+
1090
+ it('should use atomic writes for module files', () => {
1091
+ mockFs.existsSync.mockImplementation(path => {
1092
+ if (path === '/home/user/.candypack') return true
1093
+ if (path === '/home/user/.candypack/config') return true
1094
+ if (path.includes('/.bak')) return true
1095
+ return false
1096
+ })
1097
+
1098
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1099
+
1100
+ ConfigClass = require('../../core/Config.js')
1101
+ config = new ConfigClass()
1102
+ config.init()
1103
+
1104
+ mockFs.writeFileSync.mockClear()
1105
+ mockFs.renameSync.mockClear()
1106
+
1107
+ config.config.ssl = {enabled: true}
1108
+ config.force()
1109
+
1110
+ const tempWrites = mockFs.writeFileSync.mock.calls.filter(call => call[0].includes('.tmp'))
1111
+ const renames = mockFs.renameSync.mock.calls.filter(call => call[1].includes('ssl.json'))
1112
+
1113
+ expect(tempWrites.length + renames.length).toBeGreaterThan(0)
1114
+ })
1115
+
1116
+ it('should create backups before overwriting module files', () => {
1117
+ mockFs.existsSync.mockImplementation(path => {
1118
+ if (path === '/home/user/.candypack') return true
1119
+ if (path === '/home/user/.candypack/config') return true
1120
+ if (path.includes('/config/server.json')) return true
1121
+ return false
1122
+ })
1123
+
1124
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {pid: 100}}))
1125
+
1126
+ ConfigClass = require('../../core/Config.js')
1127
+ config = new ConfigClass()
1128
+ config.init()
1129
+
1130
+ mockFs.copyFileSync.mockClear()
1131
+
1132
+ config.config.server.pid = 200
1133
+ config.force()
1134
+
1135
+ const backupCalls = mockFs.copyFileSync.mock.calls.filter(call => call[1].includes('.bak'))
1136
+ expect(backupCalls.length).toBeGreaterThan(0)
1137
+ })
1138
+
1139
+ it('should fallback to single-file on modular save failure', () => {
1140
+ mockFs.existsSync.mockImplementation(path => {
1141
+ if (path === '/home/user/.candypack') return true
1142
+ if (path === '/home/user/.candypack/config') return true
1143
+ return false
1144
+ })
1145
+
1146
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1147
+
1148
+ ConfigClass = require('../../core/Config.js')
1149
+ config = new ConfigClass()
1150
+ config.init()
1151
+
1152
+ mockFs.writeFileSync.mockImplementation(() => {
1153
+ throw new Error('Disk full')
1154
+ })
1155
+
1156
+ config.config.dns = {enabled: true}
1157
+ config.force()
1158
+
1159
+ expect(config.config.server).toBeDefined()
1160
+ })
1161
+ })
1162
+
1163
+ describe('deepCompare utility', () => {
1164
+ beforeEach(() => {
1165
+ mockFs.existsSync.mockReturnValue(true)
1166
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
1167
+ ConfigClass = require('../../core/Config.js')
1168
+ config = new ConfigClass()
1169
+ config.init()
1170
+ })
1171
+
1172
+ it('should detect identical objects', () => {
1173
+ const obj1 = {a: 1, b: {c: 2}}
1174
+ const obj2 = {a: 1, b: {c: 2}}
1175
+
1176
+ // Test that identical objects are equal by comparing their JSON representation
1177
+ expect(JSON.stringify(obj1)).toBe(JSON.stringify(obj2))
1178
+ })
1179
+
1180
+ it('should detect type mismatches', () => {
1181
+ const obj1 = {a: 1}
1182
+ const obj2 = {a: '1'}
1183
+
1184
+ expect(obj1.a).not.toBe(obj2.a)
1185
+ })
1186
+
1187
+ it('should detect missing keys', () => {
1188
+ const obj1 = {a: 1, b: 2}
1189
+ const obj2 = {a: 1}
1190
+
1191
+ expect(obj1.b).toBeDefined()
1192
+ expect(obj2.b).toBeUndefined()
1193
+ })
1194
+
1195
+ it('should handle null values', () => {
1196
+ const obj1 = {a: null}
1197
+ const obj2 = {a: null}
1198
+
1199
+ expect(obj1.a).toBe(obj2.a)
1200
+ })
1201
+
1202
+ it('should handle arrays', () => {
1203
+ const obj1 = {arr: [1, 2, 3]}
1204
+ const obj2 = {arr: [1, 2, 3]}
1205
+
1206
+ expect(obj1.arr).toEqual(obj2.arr)
1207
+ })
1208
+
1209
+ it('should detect array length differences', () => {
1210
+ const obj1 = {arr: [1, 2, 3]}
1211
+ const obj2 = {arr: [1, 2]}
1212
+
1213
+ expect(obj1.arr.length).not.toBe(obj2.arr.length)
1214
+ })
1215
+
1216
+ it('should handle deeply nested objects', () => {
1217
+ const obj1 = {a: {b: {c: {d: 1}}}}
1218
+ const obj2 = {a: {b: {c: {d: 1}}}}
1219
+
1220
+ expect(JSON.stringify(obj1)).toBe(JSON.stringify(obj2))
1221
+ })
1222
+ })
1223
+
1224
+ describe('atomic write operations', () => {
1225
+ beforeEach(() => {
1226
+ process.mainModule = {path: '/mock/project'}
1227
+ })
1228
+
1229
+ it('should write to temp file first', () => {
1230
+ mockFs.existsSync.mockImplementation(path => {
1231
+ if (path === '/home/user/.candypack') return true
1232
+ if (path === '/home/user/.candypack/config') return true
1233
+ return false
1234
+ })
1235
+
1236
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1237
+
1238
+ ConfigClass = require('../../core/Config.js')
1239
+ config = new ConfigClass()
1240
+ config.init()
1241
+
1242
+ mockFs.writeFileSync.mockClear()
1243
+
1244
+ config.config.mail = {enabled: true}
1245
+ config.force()
1246
+
1247
+ const tempWrites = mockFs.writeFileSync.mock.calls.filter(call => call[0].includes('.tmp'))
1248
+ expect(tempWrites.length).toBeGreaterThan(0)
1249
+ })
1250
+
1251
+ it('should cleanup temp file on write failure', () => {
1252
+ mockFs.existsSync.mockImplementation(path => {
1253
+ if (path === '/home/user/.candypack') return true
1254
+ if (path === '/home/user/.candypack/config') return true
1255
+ if (path.includes('.tmp')) return true
1256
+ return false
1257
+ })
1258
+
1259
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1260
+
1261
+ mockFs.renameSync.mockImplementation(() => {
1262
+ throw new Error('Rename failed')
1263
+ })
1264
+
1265
+ ConfigClass = require('../../core/Config.js')
1266
+ config = new ConfigClass()
1267
+ config.init()
1268
+
1269
+ mockFs.unlinkSync.mockClear()
1270
+
1271
+ config.config.api = {enabled: true}
1272
+ config.force()
1273
+
1274
+ // Should attempt cleanup or fallback to single-file mode
1275
+ const unlinkCalls = mockFs.unlinkSync.mock.calls
1276
+ const writeFileCalls = mockFs.writeFileSync.mock.calls
1277
+
1278
+ expect(unlinkCalls.length + writeFileCalls.length).toBeGreaterThan(0)
1279
+ })
1280
+
1281
+ it('should handle ENOSPC error gracefully', () => {
1282
+ mockFs.existsSync.mockImplementation(path => {
1283
+ if (path === '/home/user/.candypack') return true
1284
+ if (path === '/home/user/.candypack/config') return true
1285
+ return false
1286
+ })
1287
+
1288
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1289
+
1290
+ mockFs.writeFileSync.mockImplementation(() => {
1291
+ const err = new Error('No space left')
1292
+ err.code = 'ENOSPC'
1293
+ throw err
1294
+ })
1295
+
1296
+ ConfigClass = require('../../core/Config.js')
1297
+ config = new ConfigClass()
1298
+ config.init()
1299
+
1300
+ config.config.service = {enabled: true}
1301
+ config.force()
1302
+
1303
+ expect(config.config.server).toBeDefined()
1304
+ })
1305
+ })
1306
+
1307
+ describe('helper methods', () => {
1308
+ it('should initialize default config for server key', () => {
1309
+ mockFs.existsSync.mockReturnValue(true)
1310
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
1311
+
1312
+ ConfigClass = require('../../core/Config.js')
1313
+ config = new ConfigClass()
1314
+ config.init()
1315
+
1316
+ const testConfig = {}
1317
+ // Access private method through reflection for testing
1318
+ const initMethod = Object.getOwnPropertyNames(Object.getPrototypeOf(config)).find(name =>
1319
+ name.includes('initializeDefaultModuleConfig')
1320
+ )
1321
+
1322
+ if (initMethod) {
1323
+ config[initMethod](testConfig, ['server'])
1324
+ expect(testConfig.server).toEqual({pid: null, started: null, watchdog: null})
1325
+ }
1326
+ })
1327
+
1328
+ it('should initialize default config for websites key', () => {
1329
+ mockFs.existsSync.mockReturnValue(true)
1330
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
1331
+
1332
+ ConfigClass = require('../../core/Config.js')
1333
+ config = new ConfigClass()
1334
+ config.init()
1335
+
1336
+ const testConfig = {}
1337
+ const initMethod = Object.getOwnPropertyNames(Object.getPrototypeOf(config)).find(name =>
1338
+ name.includes('initializeDefaultModuleConfig')
1339
+ )
1340
+
1341
+ if (initMethod) {
1342
+ config[initMethod](testConfig, ['websites'])
1343
+ expect(testConfig.websites).toEqual({})
1344
+ }
1345
+ })
1346
+
1347
+ it('should initialize default config for services key', () => {
1348
+ mockFs.existsSync.mockReturnValue(true)
1349
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
1350
+
1351
+ ConfigClass = require('../../core/Config.js')
1352
+ config = new ConfigClass()
1353
+ config.init()
1354
+
1355
+ const testConfig = {}
1356
+ const initMethod = Object.getOwnPropertyNames(Object.getPrototypeOf(config)).find(name =>
1357
+ name.includes('initializeDefaultModuleConfig')
1358
+ )
1359
+
1360
+ if (initMethod) {
1361
+ config[initMethod](testConfig, ['services'])
1362
+ expect(testConfig.services).toEqual([])
1363
+ }
1364
+ })
1365
+
1366
+ it('should not overwrite existing config values', () => {
1367
+ mockFs.existsSync.mockReturnValue(true)
1368
+ mockFs.readFileSync.mockReturnValue(createValidConfig())
1369
+
1370
+ ConfigClass = require('../../core/Config.js')
1371
+ config = new ConfigClass()
1372
+ config.init()
1373
+
1374
+ const testConfig = {server: {pid: 123}}
1375
+ const initMethod = Object.getOwnPropertyNames(Object.getPrototypeOf(config)).find(name =>
1376
+ name.includes('initializeDefaultModuleConfig')
1377
+ )
1378
+
1379
+ if (initMethod) {
1380
+ config[initMethod](testConfig, ['server'])
1381
+ expect(testConfig.server.pid).toBe(123)
1382
+ }
1383
+ })
1384
+ })
1385
+
1386
+ describe('corruption recovery', () => {
1387
+ it('should create .corrupted backup when recovering from corruption', () => {
1388
+ mockFs.existsSync.mockImplementation(path => {
1389
+ if (path === '/home/user/.candypack') return true
1390
+ if (path === '/home/user/.candypack/config') return true
1391
+ if (path.includes('/config/dns.json')) return true
1392
+ if (path.includes('/.bak/dns.json.bak')) return true
1393
+ return false
1394
+ })
1395
+
1396
+ mockFs.readFileSync.mockImplementation(path => {
1397
+ if (path.includes('dns.json') && !path.includes('.bak')) {
1398
+ throw new Error('Corrupted')
1399
+ }
1400
+ if (path.includes('dns.json.bak')) {
1401
+ return JSON.stringify({dns: {enabled: false}})
1402
+ }
1403
+ return '{}'
1404
+ })
1405
+
1406
+ ConfigClass = require('../../core/Config.js')
1407
+ config = new ConfigClass()
1408
+ config.init()
1409
+
1410
+ const corruptedCopies = mockFs.copyFileSync.mock.calls.filter(call => call[1].includes('.corrupted'))
1411
+ expect(corruptedCopies.length).toBeGreaterThan(0)
1412
+ })
1413
+
1414
+ it('should handle both main and backup being corrupted', () => {
1415
+ mockFs.existsSync.mockImplementation(path => {
1416
+ if (path === '/home/user/.candypack') return true
1417
+ if (path === '/home/user/.candypack/config') return true
1418
+ if (path.includes('/config/mail.json')) return true
1419
+ if (path.includes('/.bak/mail.json.bak')) return true
1420
+ return false
1421
+ })
1422
+
1423
+ mockFs.readFileSync.mockImplementation(() => {
1424
+ throw new Error('All corrupted')
1425
+ })
1426
+
1427
+ ConfigClass = require('../../core/Config.js')
1428
+ config = new ConfigClass()
1429
+ config.init()
1430
+
1431
+ expect(config.config.server).toBeDefined()
1432
+ })
1433
+ })
1434
+ })
1435
+ })