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.
- package/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- 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
|
+
})
|