odac 1.0.1 → 1.2.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 (143) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +33 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/auto-pr-description.yml +3 -1
  6. package/.github/workflows/release.yml +42 -1
  7. package/.github/workflows/test-coverage.yml +6 -5
  8. package/.github/workflows/test-publish.yml +36 -0
  9. package/.husky/pre-commit +10 -0
  10. package/.husky/pre-push +13 -0
  11. package/.releaserc.js +3 -3
  12. package/CHANGELOG.md +184 -0
  13. package/README.md +53 -34
  14. package/bin/odac.js +181 -49
  15. package/client/odac.js +878 -995
  16. package/docs/backend/01-overview/03-development-server.md +39 -46
  17. package/docs/backend/02-structure/01-typical-project-layout.md +59 -25
  18. package/docs/backend/03-config/00-configuration-overview.md +15 -6
  19. package/docs/backend/03-config/01-database-connection.md +3 -3
  20. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  21. package/docs/backend/03-config/03-request-timeout.md +1 -1
  22. package/docs/backend/03-config/04-environment-variables.md +4 -4
  23. package/docs/backend/03-config/05-early-hints.md +2 -2
  24. package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
  25. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  26. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  27. package/docs/backend/04-routing/09-websocket.md +29 -0
  28. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  29. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
  30. package/docs/backend/05-controllers/03-controller-classes.md +61 -55
  31. package/docs/backend/05-forms/01-custom-forms.md +103 -95
  32. package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
  33. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  34. package/docs/backend/07-views/02-rendering-a-view.md +1 -1
  35. package/docs/backend/07-views/03-variables.md +5 -5
  36. package/docs/backend/07-views/04-request-data.md +1 -1
  37. package/docs/backend/07-views/08-backend-javascript.md +1 -1
  38. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  39. package/docs/backend/08-database/01-getting-started.md +100 -0
  40. package/docs/backend/08-database/02-basics.md +136 -0
  41. package/docs/backend/08-database/03-advanced.md +84 -0
  42. package/docs/backend/08-database/04-migrations.md +48 -0
  43. package/docs/backend/09-validation/01-the-validator-service.md +1 -0
  44. package/docs/backend/10-authentication/03-register.md +9 -2
  45. package/docs/backend/10-authentication/04-odac-register-forms.md +48 -48
  46. package/docs/backend/10-authentication/05-session-management.md +16 -2
  47. package/docs/backend/10-authentication/06-odac-login-forms.md +50 -50
  48. package/docs/backend/10-authentication/07-magic-links.md +134 -0
  49. package/docs/backend/11-mail/01-the-mail-service.md +118 -28
  50. package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
  51. package/docs/backend/13-utilities/01-odac-var.md +7 -7
  52. package/docs/backend/13-utilities/02-ipc.md +73 -0
  53. package/docs/frontend/01-overview/01-introduction.md +5 -1
  54. package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
  55. package/docs/index.json +21 -125
  56. package/eslint.config.mjs +5 -47
  57. package/jest.config.js +1 -1
  58. package/package.json +16 -7
  59. package/src/Auth.js +414 -121
  60. package/src/Config.js +12 -7
  61. package/src/Database.js +188 -0
  62. package/src/Env.js +3 -1
  63. package/src/Ipc.js +337 -0
  64. package/src/Lang.js +9 -2
  65. package/src/Mail.js +408 -37
  66. package/src/Odac.js +105 -40
  67. package/src/Request.js +71 -49
  68. package/src/Route/Cron.js +62 -18
  69. package/src/Route/Internal.js +215 -12
  70. package/src/Route/Middleware.js +7 -2
  71. package/src/Route.js +372 -109
  72. package/src/Server.js +118 -12
  73. package/src/Storage.js +169 -0
  74. package/src/Token.js +6 -4
  75. package/src/Validator.js +95 -3
  76. package/src/Var.js +22 -6
  77. package/src/View/EarlyHints.js +43 -33
  78. package/src/View/Form.js +210 -28
  79. package/src/View.js +108 -7
  80. package/src/WebSocket.js +18 -3
  81. package/template/odac.json +5 -0
  82. package/template/package.json +3 -1
  83. package/template/route/www.js +12 -10
  84. package/template/view/content/home.html +3 -3
  85. package/template/view/head/main.html +2 -2
  86. package/test/Client.test.js +168 -0
  87. package/test/Config.test.js +112 -0
  88. package/test/Lang.test.js +92 -0
  89. package/test/Odac.test.js +86 -0
  90. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  91. package/test/{framework/Route.test.js → Route.test.js} +1 -1
  92. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  93. package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
  94. package/test/scripts/check-coverage.js +4 -4
  95. package/docs/backend/08-database/01-database-connection.md +0 -99
  96. package/docs/backend/08-database/02-using-mysql.md +0 -322
  97. package/src/Mysql.js +0 -575
  98. package/template/config.json +0 -5
  99. package/test/cli/Cli.test.js +0 -36
  100. package/test/core/Candy.test.js +0 -234
  101. package/test/core/Commands.test.js +0 -538
  102. package/test/core/Config.test.js +0 -1432
  103. package/test/core/Lang.test.js +0 -250
  104. package/test/core/Process.test.js +0 -156
  105. package/test/server/Api.test.js +0 -647
  106. package/test/server/DNS.test.js +0 -2050
  107. package/test/server/DNS.test.js.bak +0 -2084
  108. package/test/server/Hub.test.js +0 -497
  109. package/test/server/Log.test.js +0 -73
  110. package/test/server/Mail.account.test_.js +0 -460
  111. package/test/server/Mail.init.test_.js +0 -411
  112. package/test/server/Mail.test_.js +0 -1340
  113. package/test/server/SSL.test_.js +0 -1491
  114. package/test/server/Server.test.js +0 -765
  115. package/test/server/Service.test_.js +0 -1127
  116. package/test/server/Subdomain.test.js +0 -440
  117. package/test/server/Web/Firewall.test.js +0 -175
  118. package/test/server/Web/Proxy.test.js +0 -397
  119. package/test/server/Web.test.js +0 -1494
  120. package/test/server/__mocks__/acme-client.js +0 -17
  121. package/test/server/__mocks__/bcrypt.js +0 -50
  122. package/test/server/__mocks__/child_process.js +0 -389
  123. package/test/server/__mocks__/crypto.js +0 -432
  124. package/test/server/__mocks__/fs.js +0 -450
  125. package/test/server/__mocks__/globalOdac.js +0 -227
  126. package/test/server/__mocks__/http.js +0 -575
  127. package/test/server/__mocks__/https.js +0 -272
  128. package/test/server/__mocks__/index.js +0 -249
  129. package/test/server/__mocks__/mail/server.js +0 -100
  130. package/test/server/__mocks__/mail/smtp.js +0 -31
  131. package/test/server/__mocks__/mailparser.js +0 -81
  132. package/test/server/__mocks__/net.js +0 -369
  133. package/test/server/__mocks__/node-forge.js +0 -328
  134. package/test/server/__mocks__/os.js +0 -320
  135. package/test/server/__mocks__/path.js +0 -291
  136. package/test/server/__mocks__/selfsigned.js +0 -8
  137. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  138. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  139. package/test/server/__mocks__/smtp-server.js +0 -106
  140. package/test/server/__mocks__/sqlite3.js +0 -394
  141. package/test/server/__mocks__/testFactories.js +0 -299
  142. package/test/server/__mocks__/testHelpers.js +0 -363
  143. package/test/server/__mocks__/tls.js +0 -229
@@ -1,1432 +0,0 @@
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 Odac object
9
- global.Odac = {
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/odac/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/.odac')
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/.odac/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 odac 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 odac bin', () => {
182
- process.mainModule = {path: '/mock/node_modules/odac/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/.odac/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/.odac/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/.odac/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/.odac/.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/.odac/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/.odac/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/.odac/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/.odac/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/.odac/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/.odac/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/.odac/.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/.odac/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/.odac') return true
739
- if (path === '/home/user/.odac/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/.odac') return true
754
- if (path === '/home/user/.odac/config') return false
755
- if (path === '/home/user/.odac/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/.odac') 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/.odac') return true
786
- if (path === '/home/user/.odac/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/.odac') return true
813
- if (path === '/home/user/.odac/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/.odac') return true
827
- if (path === '/home/user/.odac/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/.odac') return true
858
- if (path === '/home/user/.odac/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/.odac') return true
880
- if (path === '/home/user/.odac/config.json') return true
881
- if (path === '/home/user/.odac/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/.odac/config') {
895
- configDirCreated = true
896
- }
897
- })
898
-
899
- mockFs.readFileSync.mockImplementation(path => {
900
- if (path === '/home/user/.odac/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/.odac/config', {recursive: true})
917
- expect(mockFs.copyFileSync).toHaveBeenCalledWith('/home/user/.odac/config.json', '/home/user/.odac/config.json.pre-modular')
918
- })
919
-
920
- it('should rollback migration on failure', () => {
921
- mockFs.existsSync.mockImplementation(path => {
922
- if (path === '/home/user/.odac') return true
923
- if (path === '/home/user/.odac/config.json') return true
924
- if (path === '/home/user/.odac/config') return false
925
- return false
926
- })
927
-
928
- mockFs.readFileSync.mockReturnValue(createValidConfig())
929
-
930
- mockFs.mkdirSync.mockImplementation(path => {
931
- if (path === '/home/user/.odac/config') {
932
- // Simulate successful directory creation
933
- mockFs.existsSync.mockImplementation(p => {
934
- if (p === path) return true
935
- if (p === '/home/user/.odac') return true
936
- if (p === '/home/user/.odac/config.json') return true
937
- return false
938
- })
939
- }
940
- })
941
-
942
- mockFs.writeFileSync.mockImplementation(() => {
943
- throw new Error('Write failed')
944
- })
945
-
946
- ConfigClass = require('../../core/Config.js')
947
- config = new ConfigClass()
948
- config.init()
949
-
950
- expect(mockFs.rmSync).toHaveBeenCalledWith('/home/user/.odac/config', {recursive: true, force: true})
951
- expect(config.config.server).toBeDefined()
952
- })
953
-
954
- it('should handle migration with permission errors', () => {
955
- mockFs.existsSync.mockImplementation(path => {
956
- if (path === '/home/user/.odac') return true
957
- if (path === '/home/user/.odac/config.json') return true
958
- if (path === '/home/user/.odac/config') return false
959
- return false
960
- })
961
-
962
- mockFs.readFileSync.mockReturnValue(createValidConfig())
963
-
964
- mockFs.mkdirSync.mockImplementation(() => {
965
- const err = new Error('Permission denied')
966
- err.code = 'EACCES'
967
- throw err
968
- })
969
-
970
- ConfigClass = require('../../core/Config.js')
971
- config = new ConfigClass()
972
- config.init()
973
-
974
- expect(config.config.server).toBeDefined()
975
- })
976
-
977
- it('should use rmSync with recursive and force options during rollback', () => {
978
- mockFs.existsSync.mockImplementation(path => {
979
- if (path === '/home/user/.odac') return true
980
- if (path === '/home/user/.odac/config.json') return true
981
- if (path === '/home/user/.odac/config') return false
982
- return false
983
- })
984
-
985
- mockFs.readFileSync.mockReturnValue(createValidConfig())
986
-
987
- let configDirCreated = false
988
- mockFs.mkdirSync.mockImplementation(path => {
989
- if (path === '/home/user/.odac/config') {
990
- configDirCreated = true
991
- mockFs.existsSync.mockImplementation(p => {
992
- if (p === path) return configDirCreated
993
- if (p === '/home/user/.odac') return true
994
- if (p === '/home/user/.odac/config.json') return true
995
- return false
996
- })
997
- }
998
- })
999
-
1000
- mockFs.writeFileSync.mockImplementation(() => {
1001
- throw new Error('Write failed')
1002
- })
1003
-
1004
- ConfigClass = require('../../core/Config.js')
1005
- config = new ConfigClass()
1006
- config.init()
1007
-
1008
- expect(mockFs.rmSync).toHaveBeenCalledWith('/home/user/.odac/config', {
1009
- recursive: true,
1010
- force: true
1011
- })
1012
- })
1013
-
1014
- it('should handle rmSync errors gracefully during rollback', () => {
1015
- mockFs.existsSync.mockImplementation(path => {
1016
- if (path === '/home/user/.odac') return true
1017
- if (path === '/home/user/.odac/config.json') return true
1018
- if (path === '/home/user/.odac/config') return false
1019
- return false
1020
- })
1021
-
1022
- mockFs.readFileSync.mockReturnValue(createValidConfig())
1023
-
1024
- mockFs.mkdirSync.mockImplementation(path => {
1025
- if (path === '/home/user/.odac/config') {
1026
- mockFs.existsSync.mockImplementation(p => {
1027
- if (p === path) return true
1028
- if (p === '/home/user/.odac') return true
1029
- if (p === '/home/user/.odac/config.json') return true
1030
- return false
1031
- })
1032
- }
1033
- })
1034
-
1035
- mockFs.writeFileSync.mockImplementation(() => {
1036
- throw new Error('Write failed')
1037
- })
1038
-
1039
- mockFs.rmSync.mockImplementation(() => {
1040
- throw new Error('rmSync failed')
1041
- })
1042
-
1043
- ConfigClass = require('../../core/Config.js')
1044
- config = new ConfigClass()
1045
-
1046
- expect(() => {
1047
- config.init()
1048
- }).not.toThrow()
1049
-
1050
- expect(config.config.server).toBeDefined()
1051
- })
1052
- })
1053
-
1054
- describe('modular saving', () => {
1055
- beforeEach(() => {
1056
- process.mainModule = {path: '/mock/project'}
1057
- })
1058
-
1059
- it('should save only changed modules', () => {
1060
- mockFs.existsSync.mockImplementation(path => {
1061
- if (path === '/home/user/.odac') return true
1062
- if (path === '/home/user/.odac/config') return true
1063
- return false
1064
- })
1065
-
1066
- mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1067
-
1068
- ConfigClass = require('../../core/Config.js')
1069
- config = new ConfigClass()
1070
- config.init()
1071
-
1072
- mockFs.writeFileSync.mockClear()
1073
- mockFs.renameSync.mockClear()
1074
-
1075
- config.config.websites = {example: {domain: 'test.com'}}
1076
- config.force()
1077
-
1078
- const writeCalls = mockFs.writeFileSync.mock.calls
1079
- const renameCalls = mockFs.renameSync.mock.calls
1080
-
1081
- const hasWebWrite = writeCalls.some(call => call[0].includes('web.json'))
1082
- const hasWebRename = renameCalls.some(call => call[1].includes('web.json'))
1083
-
1084
- expect(hasWebWrite || hasWebRename).toBe(true)
1085
- })
1086
-
1087
- it('should use atomic writes for module files', () => {
1088
- mockFs.existsSync.mockImplementation(path => {
1089
- if (path === '/home/user/.odac') return true
1090
- if (path === '/home/user/.odac/config') return true
1091
- if (path.includes('/.bak')) return true
1092
- return false
1093
- })
1094
-
1095
- mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1096
-
1097
- ConfigClass = require('../../core/Config.js')
1098
- config = new ConfigClass()
1099
- config.init()
1100
-
1101
- mockFs.writeFileSync.mockClear()
1102
- mockFs.renameSync.mockClear()
1103
-
1104
- config.config.ssl = {enabled: true}
1105
- config.force()
1106
-
1107
- const tempWrites = mockFs.writeFileSync.mock.calls.filter(call => call[0].includes('.tmp'))
1108
- const renames = mockFs.renameSync.mock.calls.filter(call => call[1].includes('ssl.json'))
1109
-
1110
- expect(tempWrites.length + renames.length).toBeGreaterThan(0)
1111
- })
1112
-
1113
- it('should create backups before overwriting module files', () => {
1114
- mockFs.existsSync.mockImplementation(path => {
1115
- if (path === '/home/user/.odac') return true
1116
- if (path === '/home/user/.odac/config') return true
1117
- if (path.includes('/config/server.json')) return true
1118
- return false
1119
- })
1120
-
1121
- mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {pid: 100}}))
1122
-
1123
- ConfigClass = require('../../core/Config.js')
1124
- config = new ConfigClass()
1125
- config.init()
1126
-
1127
- mockFs.copyFileSync.mockClear()
1128
-
1129
- config.config.server.pid = 200
1130
- config.force()
1131
-
1132
- const backupCalls = mockFs.copyFileSync.mock.calls.filter(call => call[1].includes('.bak'))
1133
- expect(backupCalls.length).toBeGreaterThan(0)
1134
- })
1135
-
1136
- it('should fallback to single-file on modular save failure', () => {
1137
- mockFs.existsSync.mockImplementation(path => {
1138
- if (path === '/home/user/.odac') return true
1139
- if (path === '/home/user/.odac/config') return true
1140
- return false
1141
- })
1142
-
1143
- mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1144
-
1145
- ConfigClass = require('../../core/Config.js')
1146
- config = new ConfigClass()
1147
- config.init()
1148
-
1149
- mockFs.writeFileSync.mockImplementation(() => {
1150
- throw new Error('Disk full')
1151
- })
1152
-
1153
- config.config.dns = {enabled: true}
1154
- config.force()
1155
-
1156
- expect(config.config.server).toBeDefined()
1157
- })
1158
- })
1159
-
1160
- describe('deepCompare utility', () => {
1161
- beforeEach(() => {
1162
- mockFs.existsSync.mockReturnValue(true)
1163
- mockFs.readFileSync.mockReturnValue(createValidConfig())
1164
- ConfigClass = require('../../core/Config.js')
1165
- config = new ConfigClass()
1166
- config.init()
1167
- })
1168
-
1169
- it('should detect identical objects', () => {
1170
- const obj1 = {a: 1, b: {c: 2}}
1171
- const obj2 = {a: 1, b: {c: 2}}
1172
-
1173
- // Test that identical objects are equal by comparing their JSON representation
1174
- expect(JSON.stringify(obj1)).toBe(JSON.stringify(obj2))
1175
- })
1176
-
1177
- it('should detect type mismatches', () => {
1178
- const obj1 = {a: 1}
1179
- const obj2 = {a: '1'}
1180
-
1181
- expect(obj1.a).not.toBe(obj2.a)
1182
- })
1183
-
1184
- it('should detect missing keys', () => {
1185
- const obj1 = {a: 1, b: 2}
1186
- const obj2 = {a: 1}
1187
-
1188
- expect(obj1.b).toBeDefined()
1189
- expect(obj2.b).toBeUndefined()
1190
- })
1191
-
1192
- it('should handle null values', () => {
1193
- const obj1 = {a: null}
1194
- const obj2 = {a: null}
1195
-
1196
- expect(obj1.a).toBe(obj2.a)
1197
- })
1198
-
1199
- it('should handle arrays', () => {
1200
- const obj1 = {arr: [1, 2, 3]}
1201
- const obj2 = {arr: [1, 2, 3]}
1202
-
1203
- expect(obj1.arr).toEqual(obj2.arr)
1204
- })
1205
-
1206
- it('should detect array length differences', () => {
1207
- const obj1 = {arr: [1, 2, 3]}
1208
- const obj2 = {arr: [1, 2]}
1209
-
1210
- expect(obj1.arr.length).not.toBe(obj2.arr.length)
1211
- })
1212
-
1213
- it('should handle deeply nested objects', () => {
1214
- const obj1 = {a: {b: {c: {d: 1}}}}
1215
- const obj2 = {a: {b: {c: {d: 1}}}}
1216
-
1217
- expect(JSON.stringify(obj1)).toBe(JSON.stringify(obj2))
1218
- })
1219
- })
1220
-
1221
- describe('atomic write operations', () => {
1222
- beforeEach(() => {
1223
- process.mainModule = {path: '/mock/project'}
1224
- })
1225
-
1226
- it('should write to temp file first', () => {
1227
- mockFs.existsSync.mockImplementation(path => {
1228
- if (path === '/home/user/.odac') return true
1229
- if (path === '/home/user/.odac/config') return true
1230
- return false
1231
- })
1232
-
1233
- mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1234
-
1235
- ConfigClass = require('../../core/Config.js')
1236
- config = new ConfigClass()
1237
- config.init()
1238
-
1239
- mockFs.writeFileSync.mockClear()
1240
-
1241
- config.config.mail = {enabled: true}
1242
- config.force()
1243
-
1244
- const tempWrites = mockFs.writeFileSync.mock.calls.filter(call => call[0].includes('.tmp'))
1245
- expect(tempWrites.length).toBeGreaterThan(0)
1246
- })
1247
-
1248
- it('should cleanup temp file on write failure', () => {
1249
- mockFs.existsSync.mockImplementation(path => {
1250
- if (path === '/home/user/.odac') return true
1251
- if (path === '/home/user/.odac/config') return true
1252
- if (path.includes('.tmp')) return true
1253
- return false
1254
- })
1255
-
1256
- mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1257
-
1258
- mockFs.renameSync.mockImplementation(() => {
1259
- throw new Error('Rename failed')
1260
- })
1261
-
1262
- ConfigClass = require('../../core/Config.js')
1263
- config = new ConfigClass()
1264
- config.init()
1265
-
1266
- mockFs.unlinkSync.mockClear()
1267
-
1268
- config.config.api = {enabled: true}
1269
- config.force()
1270
-
1271
- // Should attempt cleanup or fallback to single-file mode
1272
- const unlinkCalls = mockFs.unlinkSync.mock.calls
1273
- const writeFileCalls = mockFs.writeFileSync.mock.calls
1274
-
1275
- expect(unlinkCalls.length + writeFileCalls.length).toBeGreaterThan(0)
1276
- })
1277
-
1278
- it('should handle ENOSPC error gracefully', () => {
1279
- mockFs.existsSync.mockImplementation(path => {
1280
- if (path === '/home/user/.odac') return true
1281
- if (path === '/home/user/.odac/config') return true
1282
- return false
1283
- })
1284
-
1285
- mockFs.readFileSync.mockReturnValue(JSON.stringify({server: {}}))
1286
-
1287
- mockFs.writeFileSync.mockImplementation(() => {
1288
- const err = new Error('No space left')
1289
- err.code = 'ENOSPC'
1290
- throw err
1291
- })
1292
-
1293
- ConfigClass = require('../../core/Config.js')
1294
- config = new ConfigClass()
1295
- config.init()
1296
-
1297
- config.config.service = {enabled: true}
1298
- config.force()
1299
-
1300
- expect(config.config.server).toBeDefined()
1301
- })
1302
- })
1303
-
1304
- describe('helper methods', () => {
1305
- it('should initialize default config for server key', () => {
1306
- mockFs.existsSync.mockReturnValue(true)
1307
- mockFs.readFileSync.mockReturnValue(createValidConfig())
1308
-
1309
- ConfigClass = require('../../core/Config.js')
1310
- config = new ConfigClass()
1311
- config.init()
1312
-
1313
- const testConfig = {}
1314
- // Access private method through reflection for testing
1315
- const initMethod = Object.getOwnPropertyNames(Object.getPrototypeOf(config)).find(name =>
1316
- name.includes('initializeDefaultModuleConfig')
1317
- )
1318
-
1319
- if (initMethod) {
1320
- config[initMethod](testConfig, ['server'])
1321
- expect(testConfig.server).toEqual({pid: null, started: null, watchdog: null})
1322
- }
1323
- })
1324
-
1325
- it('should initialize default config for websites key', () => {
1326
- mockFs.existsSync.mockReturnValue(true)
1327
- mockFs.readFileSync.mockReturnValue(createValidConfig())
1328
-
1329
- ConfigClass = require('../../core/Config.js')
1330
- config = new ConfigClass()
1331
- config.init()
1332
-
1333
- const testConfig = {}
1334
- const initMethod = Object.getOwnPropertyNames(Object.getPrototypeOf(config)).find(name =>
1335
- name.includes('initializeDefaultModuleConfig')
1336
- )
1337
-
1338
- if (initMethod) {
1339
- config[initMethod](testConfig, ['websites'])
1340
- expect(testConfig.websites).toEqual({})
1341
- }
1342
- })
1343
-
1344
- it('should initialize default config for services key', () => {
1345
- mockFs.existsSync.mockReturnValue(true)
1346
- mockFs.readFileSync.mockReturnValue(createValidConfig())
1347
-
1348
- ConfigClass = require('../../core/Config.js')
1349
- config = new ConfigClass()
1350
- config.init()
1351
-
1352
- const testConfig = {}
1353
- const initMethod = Object.getOwnPropertyNames(Object.getPrototypeOf(config)).find(name =>
1354
- name.includes('initializeDefaultModuleConfig')
1355
- )
1356
-
1357
- if (initMethod) {
1358
- config[initMethod](testConfig, ['services'])
1359
- expect(testConfig.services).toEqual([])
1360
- }
1361
- })
1362
-
1363
- it('should not overwrite existing config values', () => {
1364
- mockFs.existsSync.mockReturnValue(true)
1365
- mockFs.readFileSync.mockReturnValue(createValidConfig())
1366
-
1367
- ConfigClass = require('../../core/Config.js')
1368
- config = new ConfigClass()
1369
- config.init()
1370
-
1371
- const testConfig = {server: {pid: 123}}
1372
- const initMethod = Object.getOwnPropertyNames(Object.getPrototypeOf(config)).find(name =>
1373
- name.includes('initializeDefaultModuleConfig')
1374
- )
1375
-
1376
- if (initMethod) {
1377
- config[initMethod](testConfig, ['server'])
1378
- expect(testConfig.server.pid).toBe(123)
1379
- }
1380
- })
1381
- })
1382
-
1383
- describe('corruption recovery', () => {
1384
- it('should create .corrupted backup when recovering from corruption', () => {
1385
- mockFs.existsSync.mockImplementation(path => {
1386
- if (path === '/home/user/.odac') return true
1387
- if (path === '/home/user/.odac/config') return true
1388
- if (path.includes('/config/dns.json')) return true
1389
- if (path.includes('/.bak/dns.json.bak')) return true
1390
- return false
1391
- })
1392
-
1393
- mockFs.readFileSync.mockImplementation(path => {
1394
- if (path.includes('dns.json') && !path.includes('.bak')) {
1395
- throw new Error('Corrupted')
1396
- }
1397
- if (path.includes('dns.json.bak')) {
1398
- return JSON.stringify({dns: {enabled: false}})
1399
- }
1400
- return '{}'
1401
- })
1402
-
1403
- ConfigClass = require('../../core/Config.js')
1404
- config = new ConfigClass()
1405
- config.init()
1406
-
1407
- const corruptedCopies = mockFs.copyFileSync.mock.calls.filter(call => call[1].includes('.corrupted'))
1408
- expect(corruptedCopies.length).toBeGreaterThan(0)
1409
- })
1410
-
1411
- it('should handle both main and backup being corrupted', () => {
1412
- mockFs.existsSync.mockImplementation(path => {
1413
- if (path === '/home/user/.odac') return true
1414
- if (path === '/home/user/.odac/config') return true
1415
- if (path.includes('/config/mail.json')) return true
1416
- if (path.includes('/.bak/mail.json.bak')) return true
1417
- return false
1418
- })
1419
-
1420
- mockFs.readFileSync.mockImplementation(() => {
1421
- throw new Error('All corrupted')
1422
- })
1423
-
1424
- ConfigClass = require('../../core/Config.js')
1425
- config = new ConfigClass()
1426
- config.init()
1427
-
1428
- expect(config.config.server).toBeDefined()
1429
- })
1430
- })
1431
- })
1432
- })