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,250 @@
|
|
|
1
|
+
// Mock fs before requiring Lang
|
|
2
|
+
jest.mock('fs', () => ({
|
|
3
|
+
promises: {
|
|
4
|
+
writeFile: jest.fn().mockResolvedValue()
|
|
5
|
+
},
|
|
6
|
+
readFileSync: jest.fn()
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
const fs = require('fs')
|
|
10
|
+
const path = require('path')
|
|
11
|
+
const Lang = require('../../core/Lang')
|
|
12
|
+
|
|
13
|
+
describe('Lang', () => {
|
|
14
|
+
let originalConsoleError
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset mocks before each test
|
|
18
|
+
fs.readFileSync.mockClear()
|
|
19
|
+
fs.promises.writeFile.mockClear()
|
|
20
|
+
|
|
21
|
+
// Mock console.error to prevent noise in tests
|
|
22
|
+
originalConsoleError = console.error
|
|
23
|
+
console.error = jest.fn()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
// Restore console.error
|
|
28
|
+
console.error = originalConsoleError
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should return 'CandyPack' for the 'CandyPack' key", () => {
|
|
32
|
+
const lang = new Lang()
|
|
33
|
+
expect(lang.get('CandyPack')).toBe('CandyPack')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should return the translation for an existing key', () => {
|
|
37
|
+
const mockStrings = {'test.key': 'Test Value'}
|
|
38
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(mockStrings))
|
|
39
|
+
|
|
40
|
+
const lang = new Lang()
|
|
41
|
+
expect(lang.get('test.key')).toBe('Test Value')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should replace placeholders with arguments', () => {
|
|
45
|
+
const mockStrings = {greeting: 'Hello, %s! Welcome, %s.'}
|
|
46
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(mockStrings))
|
|
47
|
+
|
|
48
|
+
const lang = new Lang()
|
|
49
|
+
expect(lang.get('greeting', 'John', 'Jane')).toBe('Hello, John! Welcome, Jane.')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should return the key and try to save it if it does not exist', () => {
|
|
53
|
+
fs.readFileSync.mockImplementation(() => {
|
|
54
|
+
throw new Error('File not found')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const lang = new Lang()
|
|
58
|
+
const key = 'non.existent.key'
|
|
59
|
+
|
|
60
|
+
expect(lang.get(key)).toBe(key)
|
|
61
|
+
|
|
62
|
+
// Verify that save was called
|
|
63
|
+
expect(fs.promises.writeFile).toHaveBeenCalled()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should handle multiple placeholders correctly', () => {
|
|
67
|
+
const mockStrings = {format: '%s %s %s'}
|
|
68
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(mockStrings))
|
|
69
|
+
const lang = new Lang()
|
|
70
|
+
expect(lang.get('format', 'a', 'b', 'c')).toBe('a b c')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('File saving behavior', () => {
|
|
74
|
+
it('should not modify existing file when all keys exist', () => {
|
|
75
|
+
const existingStrings = {
|
|
76
|
+
'existing.key1': 'Value 1',
|
|
77
|
+
'existing.key2': 'Value 2',
|
|
78
|
+
'existing.key3': 'Value 3'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(existingStrings))
|
|
82
|
+
|
|
83
|
+
const lang = new Lang()
|
|
84
|
+
|
|
85
|
+
// Access existing keys multiple times
|
|
86
|
+
lang.get('existing.key1')
|
|
87
|
+
lang.get('existing.key2')
|
|
88
|
+
lang.get('existing.key3')
|
|
89
|
+
lang.get('existing.key1') // Access again
|
|
90
|
+
|
|
91
|
+
// Verify writeFile was only called once during initialization (if file didn't exist)
|
|
92
|
+
// Since we're mocking readFileSync to return data, it should not call writeFile
|
|
93
|
+
expect(fs.promises.writeFile).not.toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should save file with correct format when new key is added', () => {
|
|
97
|
+
const existingStrings = {'existing.key': 'Existing Value'}
|
|
98
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(existingStrings))
|
|
99
|
+
|
|
100
|
+
const lang = new Lang()
|
|
101
|
+
|
|
102
|
+
// Add a new key
|
|
103
|
+
const result = lang.get('new.key')
|
|
104
|
+
|
|
105
|
+
expect(result).toBe('new.key')
|
|
106
|
+
expect(fs.promises.writeFile).toHaveBeenCalledTimes(1)
|
|
107
|
+
|
|
108
|
+
// Verify the file is saved with correct format and content
|
|
109
|
+
const [filePath, content, encoding] = fs.promises.writeFile.mock.calls[0]
|
|
110
|
+
|
|
111
|
+
expect(encoding).toBe('utf8')
|
|
112
|
+
expect(filePath).toMatch(/locale\/.*\.json$/)
|
|
113
|
+
|
|
114
|
+
const savedData = JSON.parse(content)
|
|
115
|
+
expect(savedData).toEqual({
|
|
116
|
+
'existing.key': 'Existing Value',
|
|
117
|
+
'new.key': 'new.key'
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Verify JSON formatting (4 spaces indentation)
|
|
121
|
+
expect(content).toMatch(/{\n /)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should preserve existing data when adding new keys', () => {
|
|
125
|
+
const existingStrings = {
|
|
126
|
+
key1: 'Value 1',
|
|
127
|
+
key2: 'Value 2'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(existingStrings))
|
|
131
|
+
|
|
132
|
+
const lang = new Lang()
|
|
133
|
+
|
|
134
|
+
// Add multiple new keys
|
|
135
|
+
lang.get('new.key1')
|
|
136
|
+
lang.get('new.key2')
|
|
137
|
+
|
|
138
|
+
expect(fs.promises.writeFile).toHaveBeenCalledTimes(2)
|
|
139
|
+
|
|
140
|
+
// Check the final saved state
|
|
141
|
+
const lastCall = fs.promises.writeFile.mock.calls[fs.promises.writeFile.mock.calls.length - 1]
|
|
142
|
+
const savedContent = JSON.parse(lastCall[1])
|
|
143
|
+
|
|
144
|
+
expect(savedContent).toEqual({
|
|
145
|
+
key1: 'Value 1',
|
|
146
|
+
key2: 'Value 2',
|
|
147
|
+
'new.key1': 'new.key1',
|
|
148
|
+
'new.key2': 'new.key2'
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should handle file save errors gracefully', () => {
|
|
153
|
+
fs.readFileSync.mockImplementation(() => {
|
|
154
|
+
throw new Error('File not found')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Mock writeFile to throw synchronously (simulating immediate error)
|
|
158
|
+
fs.promises.writeFile.mockImplementation(() => {
|
|
159
|
+
throw new Error('Write permission denied')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const lang = new Lang()
|
|
163
|
+
|
|
164
|
+
// This should not throw an error even if save fails
|
|
165
|
+
expect(() => lang.get('test.key')).not.toThrow()
|
|
166
|
+
|
|
167
|
+
// Verify error was logged
|
|
168
|
+
expect(console.error).toHaveBeenCalledWith('Error saving language file:', expect.any(Error))
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should create file with empty object when file does not exist', () => {
|
|
172
|
+
fs.readFileSync.mockImplementation(() => {
|
|
173
|
+
throw new Error('ENOENT: no such file or directory')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const lang = new Lang()
|
|
177
|
+
|
|
178
|
+
// Should save empty object initially
|
|
179
|
+
expect(fs.promises.writeFile).toHaveBeenCalledTimes(1)
|
|
180
|
+
|
|
181
|
+
const [filePath, content, encoding] = fs.promises.writeFile.mock.calls[0]
|
|
182
|
+
expect(content).toBe('{}')
|
|
183
|
+
expect(encoding).toBe('utf8')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should not save duplicate keys', () => {
|
|
187
|
+
const existingStrings = {'existing.key': 'Existing Value'}
|
|
188
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(existingStrings))
|
|
189
|
+
|
|
190
|
+
const lang = new Lang()
|
|
191
|
+
|
|
192
|
+
// Access the same new key multiple times
|
|
193
|
+
lang.get('new.key')
|
|
194
|
+
lang.get('new.key')
|
|
195
|
+
lang.get('new.key')
|
|
196
|
+
|
|
197
|
+
// Should only save once when the key is first added
|
|
198
|
+
expect(fs.promises.writeFile).toHaveBeenCalledTimes(1)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('Locale file path validation', () => {
|
|
203
|
+
it('should use correct locale file path', () => {
|
|
204
|
+
const mockLocale = 'en-US'
|
|
205
|
+
const originalIntl = global.Intl
|
|
206
|
+
|
|
207
|
+
// Mock Intl to return specific locale
|
|
208
|
+
global.Intl = {
|
|
209
|
+
DateTimeFormat: () => ({
|
|
210
|
+
resolvedOptions: () => ({locale: mockLocale})
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fs.readFileSync.mockImplementation(() => {
|
|
215
|
+
throw new Error('File not found')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
new Lang()
|
|
219
|
+
|
|
220
|
+
expect(fs.promises.writeFile).toHaveBeenCalledWith(expect.stringContaining(`locale/${mockLocale}.json`), expect.any(String), 'utf8')
|
|
221
|
+
|
|
222
|
+
// Restore original Intl
|
|
223
|
+
global.Intl = originalIntl
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('Mock verification', () => {
|
|
228
|
+
it('should ensure fs operations are mocked and not writing real files', () => {
|
|
229
|
+
// Verify that fs.promises.writeFile is a mock function
|
|
230
|
+
expect(jest.isMockFunction(fs.promises.writeFile)).toBe(true)
|
|
231
|
+
expect(jest.isMockFunction(fs.readFileSync)).toBe(true)
|
|
232
|
+
|
|
233
|
+
// Create a Lang instance that would normally write to file
|
|
234
|
+
fs.readFileSync.mockImplementation(() => {
|
|
235
|
+
throw new Error('File not found')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const lang = new Lang()
|
|
239
|
+
lang.get('test.mock.key')
|
|
240
|
+
|
|
241
|
+
// Verify mock was called but no real file operation occurred
|
|
242
|
+
expect(fs.promises.writeFile).toHaveBeenCalled()
|
|
243
|
+
|
|
244
|
+
// The mock should have been called with the expected parameters
|
|
245
|
+
const [filePath, content] = fs.promises.writeFile.mock.calls[0]
|
|
246
|
+
expect(filePath).toMatch(/locale\/.*\.json$/)
|
|
247
|
+
expect(content).toBe('{}')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const Process = require('../../core/Process')
|
|
2
|
+
const findProcess = require('find-process')
|
|
3
|
+
|
|
4
|
+
// Note: jest.mock is automatically hoisted.
|
|
5
|
+
// Since Process.js uses `require('find-process').default`, jest will automatically
|
|
6
|
+
// create a mock with a `default` property that is a jest.fn().
|
|
7
|
+
jest.mock('find-process')
|
|
8
|
+
const processKillSpy = jest.spyOn(process, 'kill').mockImplementation(() => {})
|
|
9
|
+
|
|
10
|
+
global.Candy = {
|
|
11
|
+
core: () => ({
|
|
12
|
+
config: {}
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('Process', () => {
|
|
17
|
+
let proc
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
proc = new Process()
|
|
21
|
+
// The mock function is on the .default property of the module mock
|
|
22
|
+
findProcess.default.mockClear()
|
|
23
|
+
processKillSpy.mockClear()
|
|
24
|
+
global.Candy.core = () => ({config: {}})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should be defined', () => {
|
|
28
|
+
expect(proc).toBeDefined()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('stop(pid)', () => {
|
|
32
|
+
it('should kill a "node" process with the given pid', async () => {
|
|
33
|
+
const pid = 123
|
|
34
|
+
findProcess.default.mockResolvedValue([{pid: pid, name: 'node'}])
|
|
35
|
+
|
|
36
|
+
await proc.stop(pid)
|
|
37
|
+
|
|
38
|
+
expect(findProcess.default).toHaveBeenCalledWith('pid', pid)
|
|
39
|
+
expect(processKillSpy).toHaveBeenCalledWith(pid, 'SIGTERM')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should not kill a process if it is not a "node" process', async () => {
|
|
43
|
+
const pid = 456
|
|
44
|
+
findProcess.default.mockResolvedValue([{pid: pid, name: 'other-process'}])
|
|
45
|
+
|
|
46
|
+
await proc.stop(pid)
|
|
47
|
+
|
|
48
|
+
expect(findProcess.default).toHaveBeenCalledWith('pid', pid)
|
|
49
|
+
expect(processKillSpy).not.toHaveBeenCalled()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should not try to kill a process if no process is found', async () => {
|
|
53
|
+
const pid = 789
|
|
54
|
+
findProcess.default.mockResolvedValue([])
|
|
55
|
+
|
|
56
|
+
await proc.stop(pid)
|
|
57
|
+
|
|
58
|
+
expect(findProcess.default).toHaveBeenCalledWith('pid', pid)
|
|
59
|
+
expect(processKillSpy).not.toHaveBeenCalled()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should resolve even if find-process throws an error', async () => {
|
|
63
|
+
const pid = 101
|
|
64
|
+
findProcess.default.mockRejectedValue(new Error('find-process failed'))
|
|
65
|
+
|
|
66
|
+
await expect(proc.stop(pid)).resolves.toBeUndefined()
|
|
67
|
+
expect(processKillSpy).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('stopAll()', () => {
|
|
72
|
+
let stopSpy
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
// It's safer to store the spy in a variable and restore it from there.
|
|
76
|
+
stopSpy = jest.spyOn(proc, 'stop').mockResolvedValue(undefined)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
stopSpy.mockRestore()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should call stop for all configured pids', async () => {
|
|
84
|
+
global.Candy.core = () => ({
|
|
85
|
+
config: {
|
|
86
|
+
server: {
|
|
87
|
+
watchdog: 100,
|
|
88
|
+
pid: 200
|
|
89
|
+
},
|
|
90
|
+
websites: {
|
|
91
|
+
'example.com': {pid: 301},
|
|
92
|
+
'test.com': {pid: 302}
|
|
93
|
+
},
|
|
94
|
+
services: [
|
|
95
|
+
{name: 'service1', pid: 401},
|
|
96
|
+
{name: 'service2', pid: 402}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
await proc.stopAll()
|
|
102
|
+
|
|
103
|
+
expect(stopSpy).toHaveBeenCalledTimes(6)
|
|
104
|
+
expect(stopSpy).toHaveBeenCalledWith(100)
|
|
105
|
+
expect(stopSpy).toHaveBeenCalledWith(200)
|
|
106
|
+
expect(stopSpy).toHaveBeenCalledWith(301)
|
|
107
|
+
expect(stopSpy).toHaveBeenCalledWith(302)
|
|
108
|
+
expect(stopSpy).toHaveBeenCalledWith(401)
|
|
109
|
+
expect(stopSpy).toHaveBeenCalledWith(402)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should handle partial configurations gracefully', async () => {
|
|
113
|
+
global.Candy.core = () => ({
|
|
114
|
+
config: {
|
|
115
|
+
server: {
|
|
116
|
+
pid: 200
|
|
117
|
+
},
|
|
118
|
+
websites: {
|
|
119
|
+
'example.com': {pid: 301}
|
|
120
|
+
},
|
|
121
|
+
services: []
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
await proc.stopAll()
|
|
126
|
+
|
|
127
|
+
expect(stopSpy).toHaveBeenCalledTimes(2)
|
|
128
|
+
expect(stopSpy).toHaveBeenCalledWith(200)
|
|
129
|
+
expect(stopSpy).toHaveBeenCalledWith(301)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should not call stop if no pids are configured', async () => {
|
|
133
|
+
global.Candy.core = () => ({
|
|
134
|
+
config: {
|
|
135
|
+
server: {},
|
|
136
|
+
websites: {},
|
|
137
|
+
services: []
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
await proc.stopAll()
|
|
142
|
+
|
|
143
|
+
expect(stopSpy).not.toHaveBeenCalled()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should handle missing top-level config keys', async () => {
|
|
147
|
+
global.Candy.core = () => ({
|
|
148
|
+
config: {}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
await proc.stopAll()
|
|
152
|
+
|
|
153
|
+
expect(stopSpy).not.toHaveBeenCalled()
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const Route = require('../../framework/src/Route')
|
|
2
|
+
|
|
3
|
+
describe('Route', () => {
|
|
4
|
+
let route
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
route = new Route()
|
|
8
|
+
global.Candy = {
|
|
9
|
+
Route: {},
|
|
10
|
+
Config: {}
|
|
11
|
+
}
|
|
12
|
+
global.__dir = process.cwd()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
delete global.Candy
|
|
17
|
+
delete global.__dir
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('check - token request', () => {
|
|
21
|
+
it('should handle token request with undefined route gracefully', async () => {
|
|
22
|
+
const mockCandy = {
|
|
23
|
+
Request: {
|
|
24
|
+
url: '/',
|
|
25
|
+
method: 'get',
|
|
26
|
+
route: 'undefined_route',
|
|
27
|
+
ssl: false,
|
|
28
|
+
host: 'example.com',
|
|
29
|
+
header: jest.fn(key => {
|
|
30
|
+
const headers = {
|
|
31
|
+
'X-Candy': 'token',
|
|
32
|
+
Referer: 'http://example.com/',
|
|
33
|
+
'X-Candy-Client': 'test-client'
|
|
34
|
+
}
|
|
35
|
+
return headers[key]
|
|
36
|
+
}),
|
|
37
|
+
cookie: jest.fn(key => {
|
|
38
|
+
if (key === 'candy_client') return 'test-client'
|
|
39
|
+
return null
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
token: jest.fn(() => 'test-token')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
route.routes = {}
|
|
46
|
+
|
|
47
|
+
const result = await route.check(mockCandy)
|
|
48
|
+
|
|
49
|
+
expect(result).toBeDefined()
|
|
50
|
+
expect(result.token).toBe('test-token')
|
|
51
|
+
expect(result.page).toBeUndefined()
|
|
52
|
+
expect(mockCandy.Request.header).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://example.com')
|
|
53
|
+
expect(mockCandy.Request.header).toHaveBeenCalledWith('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should handle token request with route but no page defined', async () => {
|
|
57
|
+
const mockCandy = {
|
|
58
|
+
Request: {
|
|
59
|
+
url: '/',
|
|
60
|
+
method: 'get',
|
|
61
|
+
route: 'test_route',
|
|
62
|
+
ssl: true,
|
|
63
|
+
host: 'example.com',
|
|
64
|
+
header: jest.fn(key => {
|
|
65
|
+
const headers = {
|
|
66
|
+
'X-Candy': 'token',
|
|
67
|
+
Referer: 'https://example.com/',
|
|
68
|
+
'X-Candy-Client': 'test-client'
|
|
69
|
+
}
|
|
70
|
+
return headers[key]
|
|
71
|
+
}),
|
|
72
|
+
cookie: jest.fn(key => {
|
|
73
|
+
if (key === 'candy_client') return 'test-client'
|
|
74
|
+
return null
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
token: jest.fn(() => 'test-token-2')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
route.routes = {
|
|
81
|
+
test_route: {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await route.check(mockCandy)
|
|
85
|
+
|
|
86
|
+
expect(result).toBeDefined()
|
|
87
|
+
expect(result.token).toBe('test-token-2')
|
|
88
|
+
expect(result.page).toBeUndefined()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should handle token request with route and page but no url match', async () => {
|
|
92
|
+
const mockCandy = {
|
|
93
|
+
Request: {
|
|
94
|
+
url: '/',
|
|
95
|
+
method: 'get',
|
|
96
|
+
route: 'test_route',
|
|
97
|
+
ssl: false,
|
|
98
|
+
host: 'example.com',
|
|
99
|
+
header: jest.fn(key => {
|
|
100
|
+
const headers = {
|
|
101
|
+
'X-Candy': 'token',
|
|
102
|
+
Referer: 'http://example.com/',
|
|
103
|
+
'X-Candy-Client': 'test-client'
|
|
104
|
+
}
|
|
105
|
+
return headers[key]
|
|
106
|
+
}),
|
|
107
|
+
cookie: jest.fn(key => {
|
|
108
|
+
if (key === 'candy_client') return 'test-client'
|
|
109
|
+
return null
|
|
110
|
+
})
|
|
111
|
+
},
|
|
112
|
+
token: jest.fn(() => 'test-token-3')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
route.routes = {
|
|
116
|
+
test_route: {
|
|
117
|
+
page: {
|
|
118
|
+
'/other': {file: 'other.js'}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await route.check(mockCandy)
|
|
124
|
+
|
|
125
|
+
expect(result).toBeDefined()
|
|
126
|
+
expect(result.token).toBe('test-token-3')
|
|
127
|
+
expect(result.page).toBeUndefined()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should not return token when referer does not match', async () => {
|
|
131
|
+
const mockCandy = {
|
|
132
|
+
Request: {
|
|
133
|
+
url: '/',
|
|
134
|
+
method: 'get',
|
|
135
|
+
route: 'test_route',
|
|
136
|
+
ssl: false,
|
|
137
|
+
host: 'example.com',
|
|
138
|
+
header: jest.fn(key => {
|
|
139
|
+
const headers = {
|
|
140
|
+
'X-Candy': 'token',
|
|
141
|
+
Referer: 'http://malicious.com/',
|
|
142
|
+
'X-Candy-Client': 'test-client'
|
|
143
|
+
}
|
|
144
|
+
return headers[key]
|
|
145
|
+
}),
|
|
146
|
+
cookie: jest.fn(key => {
|
|
147
|
+
if (key === 'candy_client') return 'test-client'
|
|
148
|
+
return null
|
|
149
|
+
}),
|
|
150
|
+
abort: jest.fn()
|
|
151
|
+
},
|
|
152
|
+
Config: {},
|
|
153
|
+
token: jest.fn(() => 'test-token')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
route.routes = {
|
|
157
|
+
test_route: {
|
|
158
|
+
page: {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await route.check(mockCandy)
|
|
163
|
+
|
|
164
|
+
expect(mockCandy.Request.header).not.toHaveBeenCalledWith('Access-Control-Allow-Origin', expect.any(String))
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should not return token when client cookie does not match', async () => {
|
|
168
|
+
const mockCandy = {
|
|
169
|
+
Request: {
|
|
170
|
+
url: '/',
|
|
171
|
+
method: 'get',
|
|
172
|
+
route: 'test_route',
|
|
173
|
+
ssl: false,
|
|
174
|
+
host: 'example.com',
|
|
175
|
+
header: jest.fn(key => {
|
|
176
|
+
const headers = {
|
|
177
|
+
'X-Candy': 'token',
|
|
178
|
+
Referer: 'http://example.com/',
|
|
179
|
+
'X-Candy-Client': 'test-client'
|
|
180
|
+
}
|
|
181
|
+
return headers[key]
|
|
182
|
+
}),
|
|
183
|
+
cookie: jest.fn(key => {
|
|
184
|
+
if (key === 'candy_client') return 'different-client'
|
|
185
|
+
return null
|
|
186
|
+
}),
|
|
187
|
+
abort: jest.fn()
|
|
188
|
+
},
|
|
189
|
+
Config: {},
|
|
190
|
+
token: jest.fn(() => 'test-token')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
route.routes = {
|
|
194
|
+
test_route: {
|
|
195
|
+
page: {}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await route.check(mockCandy)
|
|
200
|
+
|
|
201
|
+
expect(mockCandy.Request.header).not.toHaveBeenCalledWith('Access-Control-Allow-Origin', expect.any(String))
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('set', () => {
|
|
206
|
+
it('should register a route with function handler', () => {
|
|
207
|
+
global.Candy.Route.buff = 'test_route'
|
|
208
|
+
const handler = jest.fn()
|
|
209
|
+
|
|
210
|
+
route.set('get', '/test', handler)
|
|
211
|
+
|
|
212
|
+
expect(route.routes.test_route).toBeDefined()
|
|
213
|
+
expect(route.routes.test_route.get).toBeDefined()
|
|
214
|
+
expect(route.routes.test_route.get['/test']).toBeDefined()
|
|
215
|
+
expect(route.routes.test_route.get['/test'].cache).toBe(handler)
|
|
216
|
+
expect(route.routes.test_route.get['/test'].type).toBe('function')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should handle array of methods', () => {
|
|
220
|
+
global.Candy.Route.buff = 'test_route'
|
|
221
|
+
const handler = jest.fn()
|
|
222
|
+
|
|
223
|
+
route.set(['get', 'post'], '/test', handler)
|
|
224
|
+
|
|
225
|
+
expect(route.routes.test_route.get['/test']).toBeDefined()
|
|
226
|
+
expect(route.routes.test_route.post['/test']).toBeDefined()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should strip trailing slash from url', () => {
|
|
230
|
+
global.Candy.Route.buff = 'test_route'
|
|
231
|
+
const handler = jest.fn()
|
|
232
|
+
|
|
233
|
+
route.set('get', '/test/', handler)
|
|
234
|
+
|
|
235
|
+
expect(route.routes.test_route.get['/test']).toBeDefined()
|
|
236
|
+
expect(route.routes.test_route.get['/test/']).toBeUndefined()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
})
|