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,1562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Web.js module
|
|
3
|
+
* Tests web hosting, proxy functionality, and website management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Mock all required modules before importing Web
|
|
7
|
+
jest.mock('child_process')
|
|
8
|
+
jest.mock('fs')
|
|
9
|
+
jest.mock('http')
|
|
10
|
+
jest.mock('https')
|
|
11
|
+
jest.mock('http-proxy')
|
|
12
|
+
jest.mock('net')
|
|
13
|
+
jest.mock('os')
|
|
14
|
+
jest.mock('path')
|
|
15
|
+
jest.mock('tls')
|
|
16
|
+
|
|
17
|
+
const childProcess = require('child_process')
|
|
18
|
+
const fs = require('fs')
|
|
19
|
+
const http = require('http')
|
|
20
|
+
const https = require('https')
|
|
21
|
+
const httpProxy = require('http-proxy')
|
|
22
|
+
const net = require('net')
|
|
23
|
+
const os = require('os')
|
|
24
|
+
const path = require('path')
|
|
25
|
+
const tls = require('tls')
|
|
26
|
+
|
|
27
|
+
// Import test utilities
|
|
28
|
+
const {mockCandy, mockLangGet} = require('./__mocks__/globalCandy')
|
|
29
|
+
const {createMockRequest, createMockResponse} = require('./__mocks__/testFactories')
|
|
30
|
+
const {createMockWebsiteConfig} = require('./__mocks__/testFactories')
|
|
31
|
+
|
|
32
|
+
describe('Web', () => {
|
|
33
|
+
let Web
|
|
34
|
+
let mockConfig
|
|
35
|
+
let mockLog
|
|
36
|
+
let mockHttpServer
|
|
37
|
+
let mockHttpsServer
|
|
38
|
+
let mockProxyServer
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
// Reset all mocks
|
|
42
|
+
jest.clearAllMocks()
|
|
43
|
+
|
|
44
|
+
// Setup global Candy mock
|
|
45
|
+
mockCandy.resetMocks()
|
|
46
|
+
mockConfig = mockCandy.core('Config')
|
|
47
|
+
|
|
48
|
+
// Initialize config structure
|
|
49
|
+
mockConfig.config = {
|
|
50
|
+
websites: {},
|
|
51
|
+
web: {path: '/var/candypack'},
|
|
52
|
+
ssl: null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Setup Log mock
|
|
56
|
+
const mockLogInstance = {
|
|
57
|
+
log: jest.fn(),
|
|
58
|
+
error: jest.fn(),
|
|
59
|
+
info: jest.fn(),
|
|
60
|
+
debug: jest.fn()
|
|
61
|
+
}
|
|
62
|
+
mockCandy.setMock('server', 'Log', {
|
|
63
|
+
init: jest.fn().mockReturnValue(mockLogInstance)
|
|
64
|
+
})
|
|
65
|
+
mockLog = mockLogInstance.log
|
|
66
|
+
|
|
67
|
+
// Setup Api mock
|
|
68
|
+
mockCandy.setMock('server', 'Api', {
|
|
69
|
+
result: jest.fn((success, message) => ({success, message}))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Setup DNS mock with default methods
|
|
73
|
+
mockCandy.setMock('server', 'DNS', {
|
|
74
|
+
record: jest.fn(),
|
|
75
|
+
ip: '127.0.0.1'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Setup Process mock
|
|
79
|
+
mockCandy.setMock('core', 'Process', {
|
|
80
|
+
stop: jest.fn()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
global.Candy = mockCandy
|
|
84
|
+
global.__ = jest.fn((key, ...args) => {
|
|
85
|
+
// Simple mock translation function
|
|
86
|
+
let result = key
|
|
87
|
+
args.forEach((arg, index) => {
|
|
88
|
+
result = result.replace(`%s${index + 1}`, arg).replace('%s', arg)
|
|
89
|
+
})
|
|
90
|
+
return result
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Setup mock servers
|
|
94
|
+
mockHttpServer = {
|
|
95
|
+
listen: jest.fn(),
|
|
96
|
+
on: jest.fn(),
|
|
97
|
+
close: jest.fn()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
mockHttpsServer = {
|
|
101
|
+
listen: jest.fn(),
|
|
102
|
+
on: jest.fn(),
|
|
103
|
+
close: jest.fn()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
mockProxyServer = {
|
|
107
|
+
web: jest.fn(),
|
|
108
|
+
on: jest.fn()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Setup module mocks
|
|
112
|
+
http.createServer.mockReturnValue(mockHttpServer)
|
|
113
|
+
https.createServer.mockReturnValue(mockHttpsServer)
|
|
114
|
+
httpProxy.createProxyServer.mockReturnValue(mockProxyServer)
|
|
115
|
+
|
|
116
|
+
// Setup file system mocks
|
|
117
|
+
fs.existsSync.mockReturnValue(true)
|
|
118
|
+
fs.mkdirSync.mockImplementation(() => {})
|
|
119
|
+
fs.cpSync.mockImplementation(() => {})
|
|
120
|
+
fs.rmSync.mockImplementation(() => {})
|
|
121
|
+
fs.readFileSync.mockReturnValue('mock-file-content')
|
|
122
|
+
fs.writeFile.mockImplementation((path, data, callback) => {
|
|
123
|
+
if (callback) callback(null)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Setup OS mocks
|
|
127
|
+
os.homedir.mockReturnValue('/home/user')
|
|
128
|
+
os.platform.mockReturnValue('linux')
|
|
129
|
+
|
|
130
|
+
// Setup path mocks
|
|
131
|
+
path.join.mockImplementation((...args) => args.join('/'))
|
|
132
|
+
|
|
133
|
+
// Setup child process mocks
|
|
134
|
+
const mockChild = {
|
|
135
|
+
pid: 12345,
|
|
136
|
+
stdout: {on: jest.fn()},
|
|
137
|
+
stderr: {on: jest.fn()},
|
|
138
|
+
on: jest.fn()
|
|
139
|
+
}
|
|
140
|
+
childProcess.spawn.mockReturnValue(mockChild)
|
|
141
|
+
childProcess.execSync.mockImplementation(() => {})
|
|
142
|
+
|
|
143
|
+
// Setup net mocks for port checking
|
|
144
|
+
const mockNetServer = {
|
|
145
|
+
once: jest.fn(),
|
|
146
|
+
listen: jest.fn(),
|
|
147
|
+
close: jest.fn()
|
|
148
|
+
}
|
|
149
|
+
net.createServer.mockReturnValue(mockNetServer)
|
|
150
|
+
|
|
151
|
+
// Setup TLS mocks
|
|
152
|
+
const mockSecureContext = {context: 'mock-context'}
|
|
153
|
+
tls.createSecureContext.mockReturnValue(mockSecureContext)
|
|
154
|
+
|
|
155
|
+
// Import Web after mocks are set up
|
|
156
|
+
Web = require('../../server/src/Web')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
delete global.Candy
|
|
161
|
+
delete global.__
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('initialization', () => {
|
|
165
|
+
test('should initialize with default configuration', async () => {
|
|
166
|
+
await Web.init()
|
|
167
|
+
|
|
168
|
+
expect(Web.server).toBeDefined()
|
|
169
|
+
expect(Web.server).toBeDefined()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('should set default web path based on platform', async () => {
|
|
173
|
+
// Test Linux/Unix platform
|
|
174
|
+
os.platform.mockReturnValue('linux')
|
|
175
|
+
mockConfig.config.web = undefined
|
|
176
|
+
|
|
177
|
+
await Web.init()
|
|
178
|
+
|
|
179
|
+
expect(mockConfig.config.web.path).toBe('/var/candypack/')
|
|
180
|
+
|
|
181
|
+
// Test macOS platform
|
|
182
|
+
os.platform.mockReturnValue('darwin')
|
|
183
|
+
mockConfig.config.web = undefined
|
|
184
|
+
|
|
185
|
+
await Web.init()
|
|
186
|
+
|
|
187
|
+
expect(mockConfig.config.web.path).toBe('/home/user/Candypack/')
|
|
188
|
+
|
|
189
|
+
// Test Windows platform
|
|
190
|
+
os.platform.mockReturnValue('win32')
|
|
191
|
+
mockConfig.config.web = undefined
|
|
192
|
+
|
|
193
|
+
await Web.init()
|
|
194
|
+
|
|
195
|
+
expect(mockConfig.config.web.path).toBe('/home/user/Candypack/')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('should create web directory if it does not exist', async () => {
|
|
199
|
+
fs.existsSync.mockReturnValue(false)
|
|
200
|
+
mockConfig.config.web = {path: '/custom/path'}
|
|
201
|
+
|
|
202
|
+
await Web.init()
|
|
203
|
+
|
|
204
|
+
expect(fs.existsSync).toHaveBeenCalledWith('/custom/path')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('server creation', () => {
|
|
209
|
+
beforeEach(async () => {
|
|
210
|
+
await Web.init()
|
|
211
|
+
mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('should create HTTP server on port 80', () => {
|
|
215
|
+
Web.server()
|
|
216
|
+
|
|
217
|
+
expect(http.createServer).toHaveBeenCalledWith(expect.any(Function))
|
|
218
|
+
expect(mockHttpServer.listen).toHaveBeenCalledWith(80)
|
|
219
|
+
expect(mockHttpServer.on).toHaveBeenCalledWith('error', expect.any(Function))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('should handle HTTP server errors', () => {
|
|
223
|
+
// Create a fresh mock server for this test
|
|
224
|
+
const freshMockHttpServer = {
|
|
225
|
+
listen: jest.fn(),
|
|
226
|
+
on: jest.fn(),
|
|
227
|
+
close: jest.fn()
|
|
228
|
+
}
|
|
229
|
+
http.createServer.mockReturnValue(freshMockHttpServer)
|
|
230
|
+
|
|
231
|
+
// Reset the Web module's server instances to force recreation
|
|
232
|
+
Web['_Web__server_http'] = null
|
|
233
|
+
Web['_Web__server_https'] = null
|
|
234
|
+
Web['_Web__loaded'] = true // Ensure Web module is marked as loaded
|
|
235
|
+
|
|
236
|
+
// Ensure we have websites configured (required for server creation)
|
|
237
|
+
mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
|
|
238
|
+
|
|
239
|
+
Web.server()
|
|
240
|
+
|
|
241
|
+
// Verify HTTP server was created
|
|
242
|
+
expect(http.createServer).toHaveBeenCalled()
|
|
243
|
+
|
|
244
|
+
// Verify the error handler was attached
|
|
245
|
+
expect(freshMockHttpServer.on).toHaveBeenCalledWith('error', expect.any(Function))
|
|
246
|
+
|
|
247
|
+
// Get the error handler function
|
|
248
|
+
const errorCall = freshMockHttpServer.on.mock.calls.find(call => call[0] === 'error')
|
|
249
|
+
const errorHandler = errorCall[1]
|
|
250
|
+
|
|
251
|
+
const mockError = new Error('EADDRINUSE')
|
|
252
|
+
mockError.code = 'EADDRINUSE'
|
|
253
|
+
|
|
254
|
+
expect(() => errorHandler(mockError)).not.toThrow()
|
|
255
|
+
expect(mockLog).toHaveBeenCalledWith('HTTP server error: EADDRINUSE')
|
|
256
|
+
expect(mockLog).toHaveBeenCalledWith('Port 80 is already in use')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('should create HTTPS server on port 443 with SSL configuration', () => {
|
|
260
|
+
mockConfig.config.ssl = {
|
|
261
|
+
key: '/path/to/key.pem',
|
|
262
|
+
cert: '/path/to/cert.pem'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
Web.server()
|
|
266
|
+
|
|
267
|
+
expect(https.createServer).toHaveBeenCalledWith(
|
|
268
|
+
expect.objectContaining({
|
|
269
|
+
SNICallback: expect.any(Function),
|
|
270
|
+
key: 'mock-file-content',
|
|
271
|
+
cert: 'mock-file-content'
|
|
272
|
+
}),
|
|
273
|
+
expect.any(Function)
|
|
274
|
+
)
|
|
275
|
+
expect(mockHttpsServer.listen).toHaveBeenCalledWith(443)
|
|
276
|
+
expect(mockHttpsServer.on).toHaveBeenCalledWith('error', expect.any(Function))
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('should handle HTTPS server errors', () => {
|
|
280
|
+
mockConfig.config.ssl = {
|
|
281
|
+
key: '/path/to/key.pem',
|
|
282
|
+
cert: '/path/to/cert.pem'
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Create a fresh mock server for this test
|
|
286
|
+
const freshMockHttpsServer = {
|
|
287
|
+
listen: jest.fn(),
|
|
288
|
+
on: jest.fn(),
|
|
289
|
+
close: jest.fn()
|
|
290
|
+
}
|
|
291
|
+
https.createServer.mockReturnValue(freshMockHttpsServer)
|
|
292
|
+
|
|
293
|
+
// Reset the Web module's server instances to force recreation
|
|
294
|
+
Web['_Web__server_http'] = null
|
|
295
|
+
Web['_Web__server_https'] = null
|
|
296
|
+
Web['_Web__loaded'] = true // Ensure Web module is marked as loaded
|
|
297
|
+
|
|
298
|
+
// Ensure we have websites configured (required for server creation)
|
|
299
|
+
mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
|
|
300
|
+
|
|
301
|
+
Web.server()
|
|
302
|
+
|
|
303
|
+
// Verify HTTPS server was created
|
|
304
|
+
expect(https.createServer).toHaveBeenCalled()
|
|
305
|
+
|
|
306
|
+
// Verify the error handler was attached
|
|
307
|
+
expect(freshMockHttpsServer.on).toHaveBeenCalledWith('error', expect.any(Function))
|
|
308
|
+
|
|
309
|
+
// Get the error handler function
|
|
310
|
+
const errorCall = freshMockHttpsServer.on.mock.calls.find(call => call[0] === 'error')
|
|
311
|
+
const errorHandler = errorCall[1]
|
|
312
|
+
|
|
313
|
+
const mockError = new Error('EADDRINUSE')
|
|
314
|
+
mockError.code = 'EADDRINUSE'
|
|
315
|
+
|
|
316
|
+
expect(() => errorHandler(mockError)).not.toThrow()
|
|
317
|
+
expect(mockLog).toHaveBeenCalledWith('HTTPS server error: EADDRINUSE')
|
|
318
|
+
expect(mockLog).toHaveBeenCalledWith('Port 443 is already in use')
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('should not create HTTPS server without SSL configuration', () => {
|
|
322
|
+
mockConfig.config.ssl = undefined
|
|
323
|
+
|
|
324
|
+
Web.server()
|
|
325
|
+
|
|
326
|
+
expect(https.createServer).not.toHaveBeenCalled()
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('should not create HTTPS server with missing SSL files', () => {
|
|
330
|
+
mockConfig.config.ssl = {
|
|
331
|
+
key: '/path/to/key.pem',
|
|
332
|
+
cert: '/path/to/cert.pem'
|
|
333
|
+
}
|
|
334
|
+
fs.existsSync.mockImplementation(path => !path.includes('key.pem') && !path.includes('cert.pem'))
|
|
335
|
+
|
|
336
|
+
Web.server()
|
|
337
|
+
|
|
338
|
+
expect(https.createServer).not.toHaveBeenCalled()
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
describe('website creation', () => {
|
|
343
|
+
beforeEach(async () => {
|
|
344
|
+
await Web.init()
|
|
345
|
+
mockConfig.config.web = {path: '/var/candypack'}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('should create website with valid domain', () => {
|
|
349
|
+
const mockProgress = jest.fn()
|
|
350
|
+
const domain = 'example.com'
|
|
351
|
+
|
|
352
|
+
const result = Web.create(domain, mockProgress)
|
|
353
|
+
|
|
354
|
+
expect(result.success).toBe(true)
|
|
355
|
+
expect(result.message).toContain('Website example.com created')
|
|
356
|
+
expect(mockProgress).toHaveBeenCalledWith('domain', 'progress', expect.stringContaining('Setting up domain'))
|
|
357
|
+
expect(mockProgress).toHaveBeenCalledWith('domain', 'success', expect.stringContaining('Domain example.com set'))
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test('should reject invalid domain names', () => {
|
|
361
|
+
const mockProgress = jest.fn()
|
|
362
|
+
|
|
363
|
+
// Test short domain
|
|
364
|
+
let result = Web.create('ab', mockProgress)
|
|
365
|
+
expect(result.success).toBe(false)
|
|
366
|
+
expect(result.message).toBe('Invalid domain.')
|
|
367
|
+
|
|
368
|
+
// Test domain without dot (except localhost)
|
|
369
|
+
result = Web.create('invalid', mockProgress)
|
|
370
|
+
expect(result.success).toBe(false)
|
|
371
|
+
expect(result.message).toBe('Invalid domain.')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('should allow localhost as valid domain', () => {
|
|
375
|
+
const mockProgress = jest.fn()
|
|
376
|
+
|
|
377
|
+
const result = Web.create('localhost', mockProgress)
|
|
378
|
+
|
|
379
|
+
expect(result.success).toBe(true)
|
|
380
|
+
expect(result.message).toContain('Website localhost created')
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('should strip protocol prefixes from domain', () => {
|
|
384
|
+
const mockProgress = jest.fn()
|
|
385
|
+
|
|
386
|
+
Web.create('https://example.com', mockProgress)
|
|
387
|
+
|
|
388
|
+
expect(mockConfig.config.websites['example.com']).toBeDefined()
|
|
389
|
+
expect(mockConfig.config.websites['https://example.com']).toBeUndefined()
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('should reject existing domain', () => {
|
|
393
|
+
const mockProgress = jest.fn()
|
|
394
|
+
mockConfig.config.websites = {'example.com': {}}
|
|
395
|
+
|
|
396
|
+
const result = Web.create('example.com', mockProgress)
|
|
397
|
+
|
|
398
|
+
expect(result.success).toBe(false)
|
|
399
|
+
expect(result.message).toBe('Website example.com already exists.')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('should create website directory structure', () => {
|
|
403
|
+
const mockProgress = jest.fn()
|
|
404
|
+
const domain = 'example.com'
|
|
405
|
+
|
|
406
|
+
// Mock fs.existsSync to return false for the website directory so it gets created
|
|
407
|
+
fs.existsSync.mockImplementation(path => {
|
|
408
|
+
if (path === '/var/candypack/example.com') return false
|
|
409
|
+
if (path.includes('node_modules')) return false
|
|
410
|
+
return true
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
Web.create(domain, mockProgress)
|
|
414
|
+
|
|
415
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith('/var/candypack/example.com', {recursive: true})
|
|
416
|
+
expect(fs.cpSync).toHaveBeenCalledWith(expect.stringContaining('web/'), '/var/candypack/example.com', {recursive: true})
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
test('should setup npm link for candypack', () => {
|
|
420
|
+
const mockProgress = jest.fn()
|
|
421
|
+
const domain = 'example.com'
|
|
422
|
+
|
|
423
|
+
Web.create(domain, mockProgress)
|
|
424
|
+
|
|
425
|
+
expect(childProcess.execSync).toHaveBeenCalledWith('npm link candypack', {
|
|
426
|
+
cwd: '/var/candypack/example.com'
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('should remove node_modules/.bin if it exists', () => {
|
|
431
|
+
const mockProgress = jest.fn()
|
|
432
|
+
const domain = 'example.com'
|
|
433
|
+
fs.existsSync.mockImplementation(path => path.includes('node_modules/.bin'))
|
|
434
|
+
|
|
435
|
+
Web.create(domain, mockProgress)
|
|
436
|
+
|
|
437
|
+
// Note: The actual Web.js code has a bug - missing '/' in path concatenation
|
|
438
|
+
expect(fs.rmSync).toHaveBeenCalledWith('/var/candypack/example.com/node_modules/.bin', {recursive: true})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
test('should create node_modules directory if it does not exist', () => {
|
|
442
|
+
const mockProgress = jest.fn()
|
|
443
|
+
const domain = 'example.com'
|
|
444
|
+
fs.existsSync.mockImplementation(path => !path.includes('node_modules'))
|
|
445
|
+
|
|
446
|
+
Web.create(domain, mockProgress)
|
|
447
|
+
|
|
448
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith('/var/candypack/example.com/node_modules')
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('should setup DNS records for non-localhost domains', () => {
|
|
452
|
+
const mockProgress = jest.fn()
|
|
453
|
+
const domain = 'example.com'
|
|
454
|
+
const mockDNS = {
|
|
455
|
+
record: jest.fn(),
|
|
456
|
+
ip: '192.168.1.1'
|
|
457
|
+
}
|
|
458
|
+
mockCandy.setMock('server', 'DNS', mockDNS)
|
|
459
|
+
mockCandy.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
|
|
460
|
+
|
|
461
|
+
Web.create(domain, mockProgress)
|
|
462
|
+
|
|
463
|
+
expect(mockDNS.record).toHaveBeenCalledWith(
|
|
464
|
+
{name: 'example.com', type: 'A', value: '192.168.1.1'},
|
|
465
|
+
{name: 'www.example.com', type: 'CNAME', value: 'example.com'},
|
|
466
|
+
{name: 'example.com', type: 'MX', value: 'example.com'},
|
|
467
|
+
{name: 'example.com', type: 'TXT', value: 'v=spf1 a mx ip4:192.168.1.1 ~all'},
|
|
468
|
+
{
|
|
469
|
+
name: '_dmarc.example.com',
|
|
470
|
+
type: 'TXT',
|
|
471
|
+
value: 'v=DMARC1; p=reject; rua=mailto:postmaster@example.com'
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
expect(mockProgress).toHaveBeenCalledWith('dns', 'progress', expect.stringContaining('Setting up DNS records'))
|
|
475
|
+
expect(mockProgress).toHaveBeenCalledWith('dns', 'success', expect.stringContaining('DNS records for example.com set'))
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
test('should not setup DNS records for localhost', () => {
|
|
479
|
+
const mockProgress = jest.fn()
|
|
480
|
+
const mockDNS = {record: jest.fn()}
|
|
481
|
+
mockCandy.setMock('server', 'DNS', mockDNS)
|
|
482
|
+
mockCandy.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
|
|
483
|
+
|
|
484
|
+
Web.create('localhost', mockProgress)
|
|
485
|
+
|
|
486
|
+
expect(mockDNS.record).not.toHaveBeenCalled()
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
test('should not setup DNS records for IP addresses', () => {
|
|
490
|
+
const mockProgress = jest.fn()
|
|
491
|
+
const mockDNS = {record: jest.fn()}
|
|
492
|
+
mockCandy.setMock('server', 'DNS', mockDNS)
|
|
493
|
+
mockCandy.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
|
|
494
|
+
|
|
495
|
+
Web.create('192.168.1.1', mockProgress)
|
|
496
|
+
|
|
497
|
+
expect(mockDNS.record).not.toHaveBeenCalled()
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
describe('request handling and proxy functionality', () => {
|
|
502
|
+
let mockReq, mockRes
|
|
503
|
+
|
|
504
|
+
beforeEach(async () => {
|
|
505
|
+
await Web.init()
|
|
506
|
+
mockReq = createMockRequest()
|
|
507
|
+
mockRes = createMockResponse()
|
|
508
|
+
|
|
509
|
+
// Setup a test website
|
|
510
|
+
mockConfig.config.websites = {
|
|
511
|
+
'example.com': {
|
|
512
|
+
domain: 'example.com',
|
|
513
|
+
path: '/var/candypack/example.com',
|
|
514
|
+
pid: 12345,
|
|
515
|
+
port: 3000,
|
|
516
|
+
cert: {
|
|
517
|
+
ssl: {
|
|
518
|
+
key: '/path/to/example.key',
|
|
519
|
+
cert: '/path/to/example.cert'
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Mock watcher to indicate process is running
|
|
526
|
+
Web['_Web__watcher'] = {12345: true}
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
test('should redirect HTTP requests to HTTPS', () => {
|
|
530
|
+
// Verify the basic setup first
|
|
531
|
+
expect(mockConfig.config.websites['example.com']).toBeDefined()
|
|
532
|
+
expect(mockConfig.config.websites['example.com'].pid).toBe(12345)
|
|
533
|
+
expect(Web['_Web__watcher'][12345]).toBe(true)
|
|
534
|
+
|
|
535
|
+
mockReq.headers.host = 'example.com'
|
|
536
|
+
mockReq.url = '/test-path'
|
|
537
|
+
|
|
538
|
+
Web.request(mockReq, mockRes, false)
|
|
539
|
+
|
|
540
|
+
expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
|
|
541
|
+
Location: 'https://example.com/test-path'
|
|
542
|
+
})
|
|
543
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
test('should serve default index for requests without host header', () => {
|
|
547
|
+
mockReq.headers = {}
|
|
548
|
+
|
|
549
|
+
Web.request(mockReq, mockRes, true)
|
|
550
|
+
|
|
551
|
+
expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
|
|
552
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
test('should serve default index for unknown hosts', () => {
|
|
556
|
+
mockReq.headers.host = 'unknown.com'
|
|
557
|
+
|
|
558
|
+
Web.request(mockReq, mockRes, true)
|
|
559
|
+
|
|
560
|
+
expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
|
|
561
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
test('should resolve subdomain to parent domain', () => {
|
|
565
|
+
mockReq.headers.host = 'www.example.com'
|
|
566
|
+
mockReq.url = '/test'
|
|
567
|
+
|
|
568
|
+
Web.request(mockReq, mockRes, true)
|
|
569
|
+
|
|
570
|
+
expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
|
|
571
|
+
timeout: 30000,
|
|
572
|
+
proxyTimeout: 30000,
|
|
573
|
+
keepAlive: true
|
|
574
|
+
})
|
|
575
|
+
expect(mockProxyServer.web).toHaveBeenCalledWith(mockReq, mockRes, {
|
|
576
|
+
target: 'http://127.0.0.1:3000'
|
|
577
|
+
})
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
test('should proxy HTTPS requests to website process', () => {
|
|
581
|
+
mockReq.headers.host = 'example.com'
|
|
582
|
+
mockReq.url = '/api/test'
|
|
583
|
+
|
|
584
|
+
Web.request(mockReq, mockRes, true)
|
|
585
|
+
|
|
586
|
+
expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
|
|
587
|
+
timeout: 30000,
|
|
588
|
+
proxyTimeout: 30000,
|
|
589
|
+
keepAlive: true
|
|
590
|
+
})
|
|
591
|
+
expect(mockProxyServer.web).toHaveBeenCalledWith(mockReq, mockRes, {
|
|
592
|
+
target: 'http://127.0.0.1:3000'
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
test('should serve default index when website process is not running', () => {
|
|
597
|
+
mockConfig.config.websites['example.com'].pid = null
|
|
598
|
+
mockReq.headers.host = 'example.com'
|
|
599
|
+
|
|
600
|
+
Web.request(mockReq, mockRes, true)
|
|
601
|
+
|
|
602
|
+
expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
|
|
603
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
604
|
+
expect(httpProxy.createProxyServer).not.toHaveBeenCalled()
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
test('should serve default index when watcher indicates process is not running', () => {
|
|
608
|
+
Web['_Web__watcher'] = {12345: false}
|
|
609
|
+
mockReq.headers.host = 'example.com'
|
|
610
|
+
|
|
611
|
+
Web.request(mockReq, mockRes, true)
|
|
612
|
+
|
|
613
|
+
expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
|
|
614
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
615
|
+
expect(httpProxy.createProxyServer).not.toHaveBeenCalled()
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
test('should add custom headers to proxied requests', () => {
|
|
619
|
+
mockReq.headers.host = 'example.com'
|
|
620
|
+
mockReq.socket = {remoteAddress: '192.168.1.100'}
|
|
621
|
+
|
|
622
|
+
Web.request(mockReq, mockRes, true)
|
|
623
|
+
|
|
624
|
+
// Simulate proxyReq event
|
|
625
|
+
const proxyReqHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'proxyReq')[1]
|
|
626
|
+
const mockProxyReq = {
|
|
627
|
+
setHeader: jest.fn()
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
proxyReqHandler(mockProxyReq, mockReq)
|
|
631
|
+
|
|
632
|
+
expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-RemoteAddress', '192.168.1.100')
|
|
633
|
+
expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-SSL', 'true')
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
test('should handle proxy errors gracefully', () => {
|
|
637
|
+
mockReq.headers.host = 'example.com'
|
|
638
|
+
|
|
639
|
+
Web.request(mockReq, mockRes, true)
|
|
640
|
+
|
|
641
|
+
// Simulate proxy error
|
|
642
|
+
const errorHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'error')[1]
|
|
643
|
+
const mockError = new Error('Connection refused')
|
|
644
|
+
|
|
645
|
+
errorHandler(mockError, mockReq, mockRes)
|
|
646
|
+
|
|
647
|
+
expect(mockLog).toHaveBeenCalledWith('Proxy error for example.com: Connection refused')
|
|
648
|
+
expect(mockRes.statusCode).toBe(502)
|
|
649
|
+
expect(mockRes.end).toHaveBeenCalledWith('Bad Gateway')
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
test('should not set response status if headers already sent', () => {
|
|
653
|
+
mockReq.headers.host = 'example.com'
|
|
654
|
+
mockRes.headersSent = true
|
|
655
|
+
|
|
656
|
+
Web.request(mockReq, mockRes, true)
|
|
657
|
+
|
|
658
|
+
// Simulate proxy error
|
|
659
|
+
const errorHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'error')[1]
|
|
660
|
+
const mockError = new Error('Connection refused')
|
|
661
|
+
|
|
662
|
+
errorHandler(mockError, mockReq, mockRes)
|
|
663
|
+
|
|
664
|
+
expect(mockRes.statusCode).not.toBe(502)
|
|
665
|
+
expect(mockRes.end).not.toHaveBeenCalledWith('Bad Gateway')
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
test('should handle exceptions in request processing', () => {
|
|
669
|
+
mockReq.headers.host = 'example.com'
|
|
670
|
+
httpProxy.createProxyServer.mockImplementation(() => {
|
|
671
|
+
throw new Error('Proxy creation failed')
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
Web.request(mockReq, mockRes, true)
|
|
675
|
+
|
|
676
|
+
expect(mockLog).toHaveBeenCalledWith(expect.any(Error))
|
|
677
|
+
expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
|
|
678
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test('should handle HTTP requests with query parameters in redirection', () => {
|
|
682
|
+
mockReq.headers.host = 'example.com'
|
|
683
|
+
mockReq.url = '/test-path?param=value&other=123'
|
|
684
|
+
|
|
685
|
+
Web.request(mockReq, mockRes, false)
|
|
686
|
+
|
|
687
|
+
expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
|
|
688
|
+
Location: 'https://example.com/test-path?param=value&other=123'
|
|
689
|
+
})
|
|
690
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
test('should handle HTTP requests with fragments in redirection', () => {
|
|
694
|
+
mockReq.headers.host = 'example.com'
|
|
695
|
+
mockReq.url = '/test-path#section'
|
|
696
|
+
|
|
697
|
+
Web.request(mockReq, mockRes, false)
|
|
698
|
+
|
|
699
|
+
expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
|
|
700
|
+
Location: 'https://example.com/test-path#section'
|
|
701
|
+
})
|
|
702
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
test('should handle multi-level subdomain resolution', () => {
|
|
706
|
+
// Setup a multi-level subdomain scenario
|
|
707
|
+
mockConfig.config.websites = {
|
|
708
|
+
'example.com': {
|
|
709
|
+
domain: 'example.com',
|
|
710
|
+
path: '/var/candypack/example.com',
|
|
711
|
+
pid: 12345,
|
|
712
|
+
port: 3000
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
Web['_Web__watcher'] = {12345: true}
|
|
716
|
+
|
|
717
|
+
mockReq.headers.host = 'api.staging.example.com'
|
|
718
|
+
mockReq.url = '/test'
|
|
719
|
+
|
|
720
|
+
Web.request(mockReq, mockRes, true)
|
|
721
|
+
|
|
722
|
+
expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
|
|
723
|
+
timeout: 30000,
|
|
724
|
+
proxyTimeout: 30000,
|
|
725
|
+
keepAlive: true
|
|
726
|
+
})
|
|
727
|
+
expect(mockProxyServer.web).toHaveBeenCalledWith(mockReq, mockRes, {
|
|
728
|
+
target: 'http://127.0.0.1:3000'
|
|
729
|
+
})
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
test('should handle requests with port numbers in host header', () => {
|
|
733
|
+
mockReq.headers.host = 'example.com:8080'
|
|
734
|
+
mockReq.url = '/test'
|
|
735
|
+
|
|
736
|
+
Web.request(mockReq, mockRes, true)
|
|
737
|
+
|
|
738
|
+
expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
|
|
739
|
+
timeout: 30000,
|
|
740
|
+
proxyTimeout: 30000,
|
|
741
|
+
keepAlive: true
|
|
742
|
+
})
|
|
743
|
+
expect(mockProxyServer.web).toHaveBeenCalledWith(mockReq, mockRes, {
|
|
744
|
+
target: 'http://127.0.0.1:3000'
|
|
745
|
+
})
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
test('should set correct SSL header for HTTP requests', () => {
|
|
749
|
+
mockReq.headers.host = 'example.com'
|
|
750
|
+
mockReq.socket = {remoteAddress: '192.168.1.100'}
|
|
751
|
+
|
|
752
|
+
Web.request(mockReq, mockRes, false)
|
|
753
|
+
|
|
754
|
+
// HTTP request should redirect, but let's test the header logic by mocking a proxy scenario
|
|
755
|
+
// Reset mocks and test HTTPS request
|
|
756
|
+
jest.clearAllMocks()
|
|
757
|
+
|
|
758
|
+
Web.request(mockReq, mockRes, true)
|
|
759
|
+
|
|
760
|
+
// Simulate proxyReq event for HTTPS
|
|
761
|
+
const proxyReqHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'proxyReq')[1]
|
|
762
|
+
const mockProxyReq = {
|
|
763
|
+
setHeader: jest.fn()
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
proxyReqHandler(mockProxyReq, mockReq)
|
|
767
|
+
|
|
768
|
+
expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-SSL', 'true')
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
test('should handle missing remote address in proxy headers', () => {
|
|
772
|
+
mockReq.headers.host = 'example.com'
|
|
773
|
+
mockReq.socket = {} // No remoteAddress property
|
|
774
|
+
|
|
775
|
+
Web.request(mockReq, mockRes, true)
|
|
776
|
+
|
|
777
|
+
// Simulate proxyReq event
|
|
778
|
+
const proxyReqHandler = mockProxyServer.on.mock.calls.find(call => call[0] === 'proxyReq')[1]
|
|
779
|
+
const mockProxyReq = {
|
|
780
|
+
setHeader: jest.fn()
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
proxyReqHandler(mockProxyReq, mockReq)
|
|
784
|
+
|
|
785
|
+
expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-RemoteAddress', '')
|
|
786
|
+
expect(mockProxyReq.setHeader).toHaveBeenCalledWith('X-Candy-Connection-SSL', 'true')
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
test('should handle proxy timeout configuration', () => {
|
|
790
|
+
mockReq.headers.host = 'example.com'
|
|
791
|
+
|
|
792
|
+
Web.request(mockReq, mockRes, true)
|
|
793
|
+
|
|
794
|
+
expect(httpProxy.createProxyServer).toHaveBeenCalledWith({
|
|
795
|
+
timeout: 30000,
|
|
796
|
+
proxyTimeout: 30000,
|
|
797
|
+
keepAlive: true
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
describe('process management and monitoring', () => {
|
|
803
|
+
let mockChild
|
|
804
|
+
|
|
805
|
+
beforeEach(async () => {
|
|
806
|
+
await Web.init()
|
|
807
|
+
mockConfig.config.web = {path: '/var/candypack'}
|
|
808
|
+
|
|
809
|
+
// Setup mock child process
|
|
810
|
+
mockChild = {
|
|
811
|
+
pid: 12345,
|
|
812
|
+
stdout: {on: jest.fn()},
|
|
813
|
+
stderr: {on: jest.fn()},
|
|
814
|
+
on: jest.fn()
|
|
815
|
+
}
|
|
816
|
+
childProcess.spawn.mockReturnValue(mockChild)
|
|
817
|
+
|
|
818
|
+
// Initialize Web module's private properties
|
|
819
|
+
Web['_Web__active'] = {}
|
|
820
|
+
Web['_Web__error_counts'] = {}
|
|
821
|
+
Web['_Web__logs'] = {log: {}, err: {}}
|
|
822
|
+
Web['_Web__ports'] = {}
|
|
823
|
+
Web['_Web__started'] = {}
|
|
824
|
+
Web['_Web__watcher'] = {}
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
test('should test port checking functionality', async () => {
|
|
828
|
+
const mockNetServer = {
|
|
829
|
+
once: jest.fn((event, callback) => {
|
|
830
|
+
if (event === 'listening') {
|
|
831
|
+
setTimeout(() => callback(), 0)
|
|
832
|
+
}
|
|
833
|
+
}),
|
|
834
|
+
listen: jest.fn(),
|
|
835
|
+
close: jest.fn()
|
|
836
|
+
}
|
|
837
|
+
net.createServer.mockReturnValue(mockNetServer)
|
|
838
|
+
|
|
839
|
+
const result = await Web.checkPort(3000)
|
|
840
|
+
|
|
841
|
+
expect(result).toBe(true)
|
|
842
|
+
expect(mockNetServer.listen).toHaveBeenCalledWith(3000, '127.0.0.1')
|
|
843
|
+
expect(mockNetServer.close).toHaveBeenCalled()
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
test('should detect port conflicts', async () => {
|
|
847
|
+
const mockNetServer = {
|
|
848
|
+
once: jest.fn((event, callback) => {
|
|
849
|
+
if (event === 'error') {
|
|
850
|
+
setTimeout(() => callback(), 0)
|
|
851
|
+
}
|
|
852
|
+
}),
|
|
853
|
+
listen: jest.fn(),
|
|
854
|
+
close: jest.fn()
|
|
855
|
+
}
|
|
856
|
+
net.createServer.mockReturnValue(mockNetServer)
|
|
857
|
+
|
|
858
|
+
const result = await Web.checkPort(3000)
|
|
859
|
+
|
|
860
|
+
expect(result).toBe(false)
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
test('should not start process if already active', async () => {
|
|
864
|
+
const domain = 'example.com'
|
|
865
|
+
mockConfig.config.websites = {
|
|
866
|
+
[domain]: {
|
|
867
|
+
domain,
|
|
868
|
+
path: '/var/candypack/example.com'
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Mark domain as active
|
|
873
|
+
Web['_Web__active'][domain] = true
|
|
874
|
+
|
|
875
|
+
await Web.start(domain)
|
|
876
|
+
|
|
877
|
+
expect(childProcess.spawn).not.toHaveBeenCalled()
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
test('should not start process if website does not exist', async () => {
|
|
881
|
+
await Web.start('nonexistent.com')
|
|
882
|
+
|
|
883
|
+
expect(childProcess.spawn).not.toHaveBeenCalled()
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
test('should respect error cooldown period', async () => {
|
|
887
|
+
const domain = 'example.com'
|
|
888
|
+
const now = Date.now()
|
|
889
|
+
mockConfig.config.websites = {
|
|
890
|
+
[domain]: {
|
|
891
|
+
domain,
|
|
892
|
+
path: '/var/candypack/example.com',
|
|
893
|
+
status: 'errored',
|
|
894
|
+
updated: now - 500 // 500ms ago
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Set error count to 2 (should wait 2 seconds)
|
|
899
|
+
Web['_Web__error_counts'][domain] = 2
|
|
900
|
+
|
|
901
|
+
await Web.start(domain)
|
|
902
|
+
|
|
903
|
+
expect(childProcess.spawn).not.toHaveBeenCalled()
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
test('should not start process without index.js file', async () => {
|
|
907
|
+
const domain = 'example.com'
|
|
908
|
+
mockConfig.config.websites = {
|
|
909
|
+
[domain]: {
|
|
910
|
+
domain,
|
|
911
|
+
path: '/var/candypack/example.com'
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Mock index.js file as missing
|
|
916
|
+
fs.existsSync.mockImplementation(path => !path.includes('index.js'))
|
|
917
|
+
|
|
918
|
+
await Web.start(domain)
|
|
919
|
+
|
|
920
|
+
expect(childProcess.spawn).not.toHaveBeenCalled()
|
|
921
|
+
expect(mockLog).toHaveBeenCalledWith("Website example.com doesn't have index.js file.")
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
test('should automatically restart crashed processes via check method', async () => {
|
|
925
|
+
const domain = 'example.com'
|
|
926
|
+
mockConfig.config.websites = {
|
|
927
|
+
[domain]: {
|
|
928
|
+
domain,
|
|
929
|
+
path: '/var/candypack/example.com',
|
|
930
|
+
pid: null // No process running
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Spy on the start method
|
|
935
|
+
const startSpy = jest.spyOn(Web, 'start')
|
|
936
|
+
|
|
937
|
+
Web.check()
|
|
938
|
+
|
|
939
|
+
expect(startSpy).toHaveBeenCalledWith(domain)
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
test('should restart processes when watcher indicates they are not running', async () => {
|
|
943
|
+
const domain = 'example.com'
|
|
944
|
+
const pid = 12345
|
|
945
|
+
mockConfig.config.websites = {
|
|
946
|
+
[domain]: {
|
|
947
|
+
domain,
|
|
948
|
+
path: '/var/candypack/example.com',
|
|
949
|
+
pid
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Mark process as not running in watcher
|
|
954
|
+
Web['_Web__watcher'][pid] = false
|
|
955
|
+
|
|
956
|
+
const mockProcess = {
|
|
957
|
+
stop: jest.fn()
|
|
958
|
+
}
|
|
959
|
+
mockCandy.setMock('core', 'Process', mockProcess)
|
|
960
|
+
|
|
961
|
+
const startSpy = jest.spyOn(Web, 'start')
|
|
962
|
+
|
|
963
|
+
Web.check()
|
|
964
|
+
|
|
965
|
+
expect(mockProcess.stop).toHaveBeenCalledWith(pid)
|
|
966
|
+
expect(mockConfig.config.websites[domain].pid).toBeNull()
|
|
967
|
+
expect(startSpy).toHaveBeenCalledWith(domain)
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
test('should write logs to files during check', async () => {
|
|
971
|
+
const domain = 'example.com'
|
|
972
|
+
mockConfig.config.websites = {
|
|
973
|
+
[domain]: {
|
|
974
|
+
domain,
|
|
975
|
+
path: '/var/candypack/example.com',
|
|
976
|
+
pid: 12345
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Setup logs
|
|
981
|
+
Web['_Web__logs'].log[domain] = 'Test log content'
|
|
982
|
+
Web['_Web__logs'].err[domain] = 'Test error content'
|
|
983
|
+
Web['_Web__watcher'][12345] = true
|
|
984
|
+
|
|
985
|
+
os.homedir.mockReturnValue('/home/user')
|
|
986
|
+
|
|
987
|
+
Web.check()
|
|
988
|
+
|
|
989
|
+
expect(fs.writeFile).toHaveBeenCalledWith('/home/user/.candypack/logs/example.com.log', 'Test log content', expect.any(Function))
|
|
990
|
+
expect(fs.writeFile).toHaveBeenCalledWith('/var/candypack/example.com/error.log', 'Test error content', expect.any(Function))
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
test('should handle log file write errors gracefully', async () => {
|
|
994
|
+
const domain = 'example.com'
|
|
995
|
+
mockConfig.config.websites = {
|
|
996
|
+
[domain]: {
|
|
997
|
+
domain,
|
|
998
|
+
path: '/var/candypack/example.com',
|
|
999
|
+
pid: 12345
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
Web['_Web__logs'].log[domain] = 'Test log content'
|
|
1004
|
+
Web['_Web__watcher'][12345] = true
|
|
1005
|
+
|
|
1006
|
+
// Mock fs.writeFile to call callback with error
|
|
1007
|
+
fs.writeFile.mockImplementation((path, data, callback) => {
|
|
1008
|
+
callback(new Error('Write failed'))
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
Web.check()
|
|
1012
|
+
|
|
1013
|
+
// Should not throw, error should be logged
|
|
1014
|
+
expect(mockLog).toHaveBeenCalledWith(expect.any(Error))
|
|
1015
|
+
})
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
describe('website deletion and resource cleanup', () => {
|
|
1019
|
+
beforeEach(async () => {
|
|
1020
|
+
await Web.init()
|
|
1021
|
+
})
|
|
1022
|
+
|
|
1023
|
+
test('should delete website and cleanup all resources', async () => {
|
|
1024
|
+
const domain = 'example.com'
|
|
1025
|
+
const pid = 12345
|
|
1026
|
+
const port = 60000
|
|
1027
|
+
|
|
1028
|
+
mockConfig.config.websites = {
|
|
1029
|
+
[domain]: {
|
|
1030
|
+
domain,
|
|
1031
|
+
path: '/var/candypack/example.com',
|
|
1032
|
+
pid,
|
|
1033
|
+
port
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Setup internal state
|
|
1038
|
+
Web['_Web__watcher'][pid] = true
|
|
1039
|
+
Web['_Web__ports'][port] = true
|
|
1040
|
+
Web['_Web__logs'].log[domain] = 'log content'
|
|
1041
|
+
Web['_Web__logs'].err[domain] = 'error content'
|
|
1042
|
+
Web['_Web__error_counts'][domain] = 2
|
|
1043
|
+
Web['_Web__active'][domain] = false
|
|
1044
|
+
Web['_Web__started'][domain] = Date.now()
|
|
1045
|
+
|
|
1046
|
+
const mockProcess = {
|
|
1047
|
+
stop: jest.fn()
|
|
1048
|
+
}
|
|
1049
|
+
mockCandy.setMock('core', 'Process', mockProcess)
|
|
1050
|
+
|
|
1051
|
+
const result = await Web.delete(domain)
|
|
1052
|
+
|
|
1053
|
+
expect(result.success).toBe(true)
|
|
1054
|
+
expect(result.message).toContain('Website example.com deleted')
|
|
1055
|
+
expect(mockConfig.config.websites[domain]).toBeUndefined()
|
|
1056
|
+
expect(mockProcess.stop).toHaveBeenCalledWith(pid)
|
|
1057
|
+
expect(Web['_Web__watcher'][pid]).toBeUndefined()
|
|
1058
|
+
expect(Web['_Web__ports'][port]).toBeUndefined()
|
|
1059
|
+
expect(Web['_Web__logs'].log[domain]).toBeUndefined()
|
|
1060
|
+
expect(Web['_Web__logs'].err[domain]).toBeUndefined()
|
|
1061
|
+
expect(Web['_Web__error_counts'][domain]).toBeUndefined()
|
|
1062
|
+
expect(Web['_Web__active'][domain]).toBeUndefined()
|
|
1063
|
+
expect(Web['_Web__started'][domain]).toBeUndefined()
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
test('should handle deletion of non-existent website', async () => {
|
|
1067
|
+
const result = await Web.delete('nonexistent.com')
|
|
1068
|
+
|
|
1069
|
+
expect(result.success).toBe(false)
|
|
1070
|
+
expect(result.message).toContain('Website nonexistent.com not found')
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
test('should handle deletion of website without running process', async () => {
|
|
1074
|
+
const domain = 'example.com'
|
|
1075
|
+
|
|
1076
|
+
mockConfig.config.websites = {
|
|
1077
|
+
[domain]: {
|
|
1078
|
+
domain,
|
|
1079
|
+
path: '/var/candypack/example.com',
|
|
1080
|
+
pid: null // No process running
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const result = await Web.delete(domain)
|
|
1085
|
+
|
|
1086
|
+
expect(result.success).toBe(true)
|
|
1087
|
+
expect(result.message).toContain('Website example.com deleted')
|
|
1088
|
+
expect(mockConfig.config.websites[domain]).toBeUndefined()
|
|
1089
|
+
})
|
|
1090
|
+
|
|
1091
|
+
test('should strip protocol prefixes from domain in deletion', async () => {
|
|
1092
|
+
const domain = 'example.com'
|
|
1093
|
+
|
|
1094
|
+
mockConfig.config.websites = {
|
|
1095
|
+
[domain]: {
|
|
1096
|
+
domain,
|
|
1097
|
+
path: '/var/candypack/example.com'
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const result = await Web.delete('https://example.com')
|
|
1102
|
+
|
|
1103
|
+
expect(result.success).toBe(true)
|
|
1104
|
+
expect(mockConfig.config.websites[domain]).toBeUndefined()
|
|
1105
|
+
})
|
|
1106
|
+
|
|
1107
|
+
test('should stop all website processes via stopAll method', () => {
|
|
1108
|
+
const domain1 = 'example.com'
|
|
1109
|
+
const domain2 = 'test.com'
|
|
1110
|
+
const pid1 = 12345
|
|
1111
|
+
const pid2 = 67890
|
|
1112
|
+
|
|
1113
|
+
mockConfig.config.websites = {
|
|
1114
|
+
[domain1]: {domain: domain1, pid: pid1},
|
|
1115
|
+
[domain2]: {domain: domain2, pid: pid2}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const mockProcess = {
|
|
1119
|
+
stop: jest.fn()
|
|
1120
|
+
}
|
|
1121
|
+
mockCandy.setMock('core', 'Process', mockProcess)
|
|
1122
|
+
|
|
1123
|
+
Web.stopAll()
|
|
1124
|
+
|
|
1125
|
+
expect(mockProcess.stop).toHaveBeenCalledWith(pid1)
|
|
1126
|
+
expect(mockProcess.stop).toHaveBeenCalledWith(pid2)
|
|
1127
|
+
expect(mockConfig.config.websites[domain1].pid).toBeNull()
|
|
1128
|
+
expect(mockConfig.config.websites[domain2].pid).toBeNull()
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
test('should handle stopAll with no websites', () => {
|
|
1132
|
+
mockConfig.config.websites = {}
|
|
1133
|
+
|
|
1134
|
+
const mockProcess = {
|
|
1135
|
+
stop: jest.fn()
|
|
1136
|
+
}
|
|
1137
|
+
mockCandy.setMock('core', 'Process', mockProcess)
|
|
1138
|
+
|
|
1139
|
+
expect(() => Web.stopAll()).not.toThrow()
|
|
1140
|
+
expect(mockProcess.stop).not.toHaveBeenCalled()
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
test('should handle stopAll with websites that have no running processes', () => {
|
|
1144
|
+
const domain = 'example.com'
|
|
1145
|
+
|
|
1146
|
+
mockConfig.config.websites = {
|
|
1147
|
+
[domain]: {domain, pid: null}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const mockProcess = {
|
|
1151
|
+
stop: jest.fn()
|
|
1152
|
+
}
|
|
1153
|
+
mockCandy.setMock('core', 'Process', mockProcess)
|
|
1154
|
+
|
|
1155
|
+
Web.stopAll()
|
|
1156
|
+
|
|
1157
|
+
expect(mockProcess.stop).not.toHaveBeenCalled()
|
|
1158
|
+
})
|
|
1159
|
+
})
|
|
1160
|
+
|
|
1161
|
+
describe('SSL certificate handling and SNI', () => {
|
|
1162
|
+
beforeEach(async () => {
|
|
1163
|
+
await Web.init()
|
|
1164
|
+
mockConfig.config.ssl = {
|
|
1165
|
+
key: '/path/to/default.key',
|
|
1166
|
+
cert: '/path/to/default.cert'
|
|
1167
|
+
}
|
|
1168
|
+
mockConfig.config.websites = {
|
|
1169
|
+
'example.com': {
|
|
1170
|
+
domain: 'example.com',
|
|
1171
|
+
cert: {
|
|
1172
|
+
ssl: {
|
|
1173
|
+
key: '/path/to/example.key',
|
|
1174
|
+
cert: '/path/to/example.cert'
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
'test.com': {
|
|
1179
|
+
domain: 'test.com',
|
|
1180
|
+
cert: false
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
test('should use website-specific SSL certificate via SNI', () => {
|
|
1186
|
+
Web.server()
|
|
1187
|
+
|
|
1188
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1189
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1190
|
+
const mockCallback = jest.fn()
|
|
1191
|
+
|
|
1192
|
+
sniCallback('example.com', mockCallback)
|
|
1193
|
+
|
|
1194
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
|
|
1195
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
|
|
1196
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1197
|
+
key: 'mock-file-content',
|
|
1198
|
+
cert: 'mock-file-content'
|
|
1199
|
+
})
|
|
1200
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
test('should fall back to default SSL certificate for websites without specific certs', () => {
|
|
1204
|
+
Web.server()
|
|
1205
|
+
|
|
1206
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1207
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1208
|
+
const mockCallback = jest.fn()
|
|
1209
|
+
|
|
1210
|
+
sniCallback('test.com', mockCallback)
|
|
1211
|
+
|
|
1212
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1213
|
+
key: 'mock-file-content',
|
|
1214
|
+
cert: 'mock-file-content'
|
|
1215
|
+
})
|
|
1216
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1217
|
+
})
|
|
1218
|
+
|
|
1219
|
+
test('should resolve subdomain to parent domain for SSL certificate', () => {
|
|
1220
|
+
Web.server()
|
|
1221
|
+
|
|
1222
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1223
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1224
|
+
const mockCallback = jest.fn()
|
|
1225
|
+
|
|
1226
|
+
sniCallback('www.example.com', mockCallback)
|
|
1227
|
+
|
|
1228
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
|
|
1229
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
|
|
1230
|
+
})
|
|
1231
|
+
|
|
1232
|
+
test('should use default certificate for unknown domains', () => {
|
|
1233
|
+
Web.server()
|
|
1234
|
+
|
|
1235
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1236
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1237
|
+
const mockCallback = jest.fn()
|
|
1238
|
+
|
|
1239
|
+
sniCallback('unknown.com', mockCallback)
|
|
1240
|
+
|
|
1241
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1242
|
+
key: 'mock-file-content',
|
|
1243
|
+
cert: 'mock-file-content'
|
|
1244
|
+
})
|
|
1245
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1246
|
+
})
|
|
1247
|
+
|
|
1248
|
+
test('should handle SSL certificate file read errors', () => {
|
|
1249
|
+
fs.readFileSync.mockImplementation(path => {
|
|
1250
|
+
if (path.includes('example.key')) {
|
|
1251
|
+
throw new Error('File not found')
|
|
1252
|
+
}
|
|
1253
|
+
return 'mock-file-content'
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1256
|
+
Web.server()
|
|
1257
|
+
|
|
1258
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1259
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1260
|
+
const mockCallback = jest.fn()
|
|
1261
|
+
|
|
1262
|
+
sniCallback('example.com', mockCallback)
|
|
1263
|
+
|
|
1264
|
+
expect(mockLog).toHaveBeenCalledWith('SSL certificate error for example.com: File not found')
|
|
1265
|
+
expect(mockCallback).toHaveBeenCalledWith(expect.any(Error))
|
|
1266
|
+
})
|
|
1267
|
+
|
|
1268
|
+
test('should handle missing SSL certificate files gracefully', () => {
|
|
1269
|
+
fs.existsSync.mockImplementation(path => !path.includes('example.key') && !path.includes('example.cert'))
|
|
1270
|
+
|
|
1271
|
+
Web.server()
|
|
1272
|
+
|
|
1273
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1274
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1275
|
+
const mockCallback = jest.fn()
|
|
1276
|
+
|
|
1277
|
+
sniCallback('example.com', mockCallback)
|
|
1278
|
+
|
|
1279
|
+
// Should fall back to default certificate
|
|
1280
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1281
|
+
key: 'mock-file-content',
|
|
1282
|
+
cert: 'mock-file-content'
|
|
1283
|
+
})
|
|
1284
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1285
|
+
})
|
|
1286
|
+
|
|
1287
|
+
test('should handle multi-level subdomain SSL certificate resolution', () => {
|
|
1288
|
+
Web.server()
|
|
1289
|
+
|
|
1290
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1291
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1292
|
+
const mockCallback = jest.fn()
|
|
1293
|
+
|
|
1294
|
+
// Test multi-level subdomain resolution (api.staging.example.com -> example.com)
|
|
1295
|
+
sniCallback('api.staging.example.com', mockCallback)
|
|
1296
|
+
|
|
1297
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
|
|
1298
|
+
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
|
|
1299
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1300
|
+
key: 'mock-file-content',
|
|
1301
|
+
cert: 'mock-file-content'
|
|
1302
|
+
})
|
|
1303
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
test('should handle SSL certificate with missing cert property', () => {
|
|
1307
|
+
mockConfig.config.websites['example.com'].cert = {
|
|
1308
|
+
ssl: {
|
|
1309
|
+
key: '/path/to/example.key'
|
|
1310
|
+
// Missing cert property
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
Web.server()
|
|
1315
|
+
|
|
1316
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1317
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1318
|
+
const mockCallback = jest.fn()
|
|
1319
|
+
|
|
1320
|
+
sniCallback('example.com', mockCallback)
|
|
1321
|
+
|
|
1322
|
+
// Should fall back to default certificate
|
|
1323
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1324
|
+
key: 'mock-file-content',
|
|
1325
|
+
cert: 'mock-file-content'
|
|
1326
|
+
})
|
|
1327
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
test('should handle SSL certificate with missing key property', () => {
|
|
1331
|
+
mockConfig.config.websites['example.com'].cert = {
|
|
1332
|
+
ssl: {
|
|
1333
|
+
cert: '/path/to/example.cert'
|
|
1334
|
+
// Missing key property
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
Web.server()
|
|
1339
|
+
|
|
1340
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1341
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1342
|
+
const mockCallback = jest.fn()
|
|
1343
|
+
|
|
1344
|
+
sniCallback('example.com', mockCallback)
|
|
1345
|
+
|
|
1346
|
+
// Should fall back to default certificate
|
|
1347
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1348
|
+
key: 'mock-file-content',
|
|
1349
|
+
cert: 'mock-file-content'
|
|
1350
|
+
})
|
|
1351
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
test('should handle SSL certificate with missing ssl property', () => {
|
|
1355
|
+
mockConfig.config.websites['example.com'].cert = {
|
|
1356
|
+
// Missing ssl property
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
Web.server()
|
|
1360
|
+
|
|
1361
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1362
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1363
|
+
const mockCallback = jest.fn()
|
|
1364
|
+
|
|
1365
|
+
sniCallback('example.com', mockCallback)
|
|
1366
|
+
|
|
1367
|
+
// Should fall back to default certificate
|
|
1368
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1369
|
+
key: 'mock-file-content',
|
|
1370
|
+
cert: 'mock-file-content'
|
|
1371
|
+
})
|
|
1372
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1373
|
+
})
|
|
1374
|
+
|
|
1375
|
+
test('should handle hostname without dots in SNI callback', () => {
|
|
1376
|
+
Web.server()
|
|
1377
|
+
|
|
1378
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1379
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1380
|
+
const mockCallback = jest.fn()
|
|
1381
|
+
|
|
1382
|
+
sniCallback('localhost', mockCallback)
|
|
1383
|
+
|
|
1384
|
+
// Should use default certificate for localhost
|
|
1385
|
+
expect(tls.createSecureContext).toHaveBeenCalledWith({
|
|
1386
|
+
key: 'mock-file-content',
|
|
1387
|
+
cert: 'mock-file-content'
|
|
1388
|
+
})
|
|
1389
|
+
expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
|
|
1390
|
+
})
|
|
1391
|
+
|
|
1392
|
+
test('should handle tls.createSecureContext errors', () => {
|
|
1393
|
+
tls.createSecureContext.mockImplementation(() => {
|
|
1394
|
+
throw new Error('Invalid certificate format')
|
|
1395
|
+
})
|
|
1396
|
+
|
|
1397
|
+
Web.server()
|
|
1398
|
+
|
|
1399
|
+
const httpsOptions = https.createServer.mock.calls[0][0]
|
|
1400
|
+
const sniCallback = httpsOptions.SNICallback
|
|
1401
|
+
const mockCallback = jest.fn()
|
|
1402
|
+
|
|
1403
|
+
sniCallback('example.com', mockCallback)
|
|
1404
|
+
|
|
1405
|
+
expect(mockLog).toHaveBeenCalledWith('SSL certificate error for example.com: Invalid certificate format')
|
|
1406
|
+
expect(mockCallback).toHaveBeenCalledWith(expect.any(Error))
|
|
1407
|
+
})
|
|
1408
|
+
})
|
|
1409
|
+
|
|
1410
|
+
describe('website deletion', () => {
|
|
1411
|
+
beforeEach(async () => {
|
|
1412
|
+
await Web.init()
|
|
1413
|
+
mockConfig.config.websites = {
|
|
1414
|
+
'example.com': {
|
|
1415
|
+
domain: 'example.com',
|
|
1416
|
+
path: '/var/candypack/example.com',
|
|
1417
|
+
pid: 12345,
|
|
1418
|
+
port: 3000
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
Web['_Web__watcher'] = {12345: true}
|
|
1422
|
+
Web['_Web__ports'] = {3000: true}
|
|
1423
|
+
Web['_Web__logs'] = {
|
|
1424
|
+
log: {'example.com': 'log content'},
|
|
1425
|
+
err: {'example.com': 'error content'}
|
|
1426
|
+
}
|
|
1427
|
+
Web['_Web__error_counts'] = {'example.com': 2}
|
|
1428
|
+
Web['_Web__active'] = {'example.com': false}
|
|
1429
|
+
Web['_Web__started'] = {'example.com': Date.now()}
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
test('should delete website and cleanup all resources', async () => {
|
|
1433
|
+
const mockProcess = {
|
|
1434
|
+
stop: jest.fn()
|
|
1435
|
+
}
|
|
1436
|
+
mockCandy.setMock('core', 'Process', mockProcess)
|
|
1437
|
+
|
|
1438
|
+
const result = await Web.delete('example.com')
|
|
1439
|
+
|
|
1440
|
+
expect(result.success).toBe(true)
|
|
1441
|
+
expect(result.message).toBe('Website example.com deleted.')
|
|
1442
|
+
expect(mockConfig.config.websites['example.com']).toBeUndefined()
|
|
1443
|
+
expect(mockProcess.stop).toHaveBeenCalledWith(12345)
|
|
1444
|
+
expect(Web['_Web__watcher'][12345]).toBeUndefined()
|
|
1445
|
+
expect(Web['_Web__ports'][3000]).toBeUndefined()
|
|
1446
|
+
expect(Web['_Web__logs'].log['example.com']).toBeUndefined()
|
|
1447
|
+
expect(Web['_Web__logs'].err['example.com']).toBeUndefined()
|
|
1448
|
+
expect(Web['_Web__error_counts']['example.com']).toBeUndefined()
|
|
1449
|
+
expect(Web['_Web__active']['example.com']).toBeUndefined()
|
|
1450
|
+
expect(Web['_Web__started']['example.com']).toBeUndefined()
|
|
1451
|
+
})
|
|
1452
|
+
|
|
1453
|
+
test('should strip protocol prefixes before deletion', async () => {
|
|
1454
|
+
const result = await Web.delete('https://example.com')
|
|
1455
|
+
|
|
1456
|
+
expect(result.success).toBe(true)
|
|
1457
|
+
expect(mockConfig.config.websites['example.com']).toBeUndefined()
|
|
1458
|
+
})
|
|
1459
|
+
|
|
1460
|
+
test('should return error for non-existent website', async () => {
|
|
1461
|
+
const result = await Web.delete('nonexistent.com')
|
|
1462
|
+
|
|
1463
|
+
expect(result.success).toBe(false)
|
|
1464
|
+
expect(result.message).toBe('Website nonexistent.com not found.')
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
test('should handle deletion of website without running process', async () => {
|
|
1468
|
+
mockConfig.config.websites['example.com'].pid = null
|
|
1469
|
+
|
|
1470
|
+
const result = await Web.delete('example.com')
|
|
1471
|
+
|
|
1472
|
+
expect(result.success).toBe(true)
|
|
1473
|
+
expect(mockConfig.config.websites['example.com']).toBeUndefined()
|
|
1474
|
+
})
|
|
1475
|
+
})
|
|
1476
|
+
|
|
1477
|
+
describe('utility methods', () => {
|
|
1478
|
+
beforeEach(async () => {
|
|
1479
|
+
await Web.init()
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
test('should list all websites', async () => {
|
|
1483
|
+
mockConfig.config.websites = {
|
|
1484
|
+
'example.com': {},
|
|
1485
|
+
'test.com': {},
|
|
1486
|
+
'demo.com': {}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const result = await Web.list()
|
|
1490
|
+
|
|
1491
|
+
expect(result.success).toBe(true)
|
|
1492
|
+
expect(result.message).toContain('example.com')
|
|
1493
|
+
expect(result.message).toContain('test.com')
|
|
1494
|
+
expect(result.message).toContain('demo.com')
|
|
1495
|
+
})
|
|
1496
|
+
|
|
1497
|
+
test('should return error when no websites exist', async () => {
|
|
1498
|
+
mockConfig.config.websites = {}
|
|
1499
|
+
|
|
1500
|
+
const result = await Web.list()
|
|
1501
|
+
|
|
1502
|
+
expect(result.success).toBe(false)
|
|
1503
|
+
expect(result.message).toBe('No websites found.')
|
|
1504
|
+
})
|
|
1505
|
+
|
|
1506
|
+
test('should return website status', async () => {
|
|
1507
|
+
mockConfig.config.websites = {
|
|
1508
|
+
'example.com': {
|
|
1509
|
+
status: 'running',
|
|
1510
|
+
pid: 12345
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
const result = await Web.status()
|
|
1515
|
+
|
|
1516
|
+
expect(result).toEqual(mockConfig.config.websites)
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
test('should set website configuration', () => {
|
|
1520
|
+
const websiteData = {
|
|
1521
|
+
domain: 'example.com',
|
|
1522
|
+
path: '/var/candypack/example.com',
|
|
1523
|
+
status: 'running'
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
Web.set('example.com', websiteData)
|
|
1527
|
+
|
|
1528
|
+
expect(mockConfig.config.websites['example.com']).toEqual(websiteData)
|
|
1529
|
+
})
|
|
1530
|
+
|
|
1531
|
+
test('should stop all websites', () => {
|
|
1532
|
+
mockConfig.config.websites = {
|
|
1533
|
+
'example.com': {pid: 12345},
|
|
1534
|
+
'test.com': {pid: 67890},
|
|
1535
|
+
'demo.com': {pid: null}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const mockProcess = {
|
|
1539
|
+
stop: jest.fn()
|
|
1540
|
+
}
|
|
1541
|
+
mockCandy.setMock('core', 'Process', mockProcess)
|
|
1542
|
+
|
|
1543
|
+
Web.stopAll()
|
|
1544
|
+
|
|
1545
|
+
expect(mockProcess.stop).toHaveBeenCalledWith(12345)
|
|
1546
|
+
expect(mockProcess.stop).toHaveBeenCalledWith(67890)
|
|
1547
|
+
expect(mockProcess.stop).toHaveBeenCalledTimes(2)
|
|
1548
|
+
expect(mockConfig.config.websites['example.com'].pid).toBe(null)
|
|
1549
|
+
expect(mockConfig.config.websites['test.com'].pid).toBe(null)
|
|
1550
|
+
})
|
|
1551
|
+
|
|
1552
|
+
test('should serve default index page', () => {
|
|
1553
|
+
const mockReq = createMockRequest()
|
|
1554
|
+
const mockRes = createMockResponse()
|
|
1555
|
+
|
|
1556
|
+
Web.index(mockReq, mockRes)
|
|
1557
|
+
|
|
1558
|
+
expect(mockRes.write).toHaveBeenCalledWith('CandyPack Server')
|
|
1559
|
+
expect(mockRes.end).toHaveBeenCalled()
|
|
1560
|
+
})
|
|
1561
|
+
})
|
|
1562
|
+
})
|