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,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Subdomain.js module
|
|
3
|
+
* Tests subdomain creation, deletion, listing, and DNS integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {setupGlobalMocks, cleanupGlobalMocks} = require('./__mocks__/testHelpers')
|
|
7
|
+
const {createMockWebsiteConfig} = require('./__mocks__/testFactories')
|
|
8
|
+
|
|
9
|
+
// Create mock log functions first
|
|
10
|
+
const mockLog = jest.fn()
|
|
11
|
+
const mockError = jest.fn()
|
|
12
|
+
|
|
13
|
+
describe('Subdomain', () => {
|
|
14
|
+
let Subdomain
|
|
15
|
+
let mockConfig
|
|
16
|
+
let mockDNS
|
|
17
|
+
let mockSSL
|
|
18
|
+
let mockApi
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
setupGlobalMocks()
|
|
22
|
+
|
|
23
|
+
// Set up the Log mock before requiring Subdomain
|
|
24
|
+
const {mockCandy} = require('./__mocks__/globalCandy')
|
|
25
|
+
mockCandy.setMock('core', 'Log', {
|
|
26
|
+
init: jest.fn().mockReturnValue({
|
|
27
|
+
log: mockLog,
|
|
28
|
+
error: mockError
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Create mock website configurations
|
|
33
|
+
mockConfig = {
|
|
34
|
+
websites: {
|
|
35
|
+
'example.com': createMockWebsiteConfig('example.com', {
|
|
36
|
+
subdomain: ['www']
|
|
37
|
+
}),
|
|
38
|
+
'test.org': createMockWebsiteConfig('test.org', {
|
|
39
|
+
subdomain: ['www', 'mail']
|
|
40
|
+
}),
|
|
41
|
+
'www.example.com': createMockWebsiteConfig('www.example.com', {
|
|
42
|
+
subdomain: ['www']
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Set up mocks for dependencies
|
|
48
|
+
mockDNS = {
|
|
49
|
+
record: jest.fn(),
|
|
50
|
+
delete: jest.fn()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
mockSSL = {
|
|
54
|
+
renew: jest.fn()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
mockApi = {
|
|
58
|
+
result: jest.fn((success, data) => ({success, data}))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Configure global Candy mocks
|
|
62
|
+
global.Candy.setMock('core', 'Config', {config: mockConfig})
|
|
63
|
+
global.Candy.setMock('server', 'DNS', mockDNS)
|
|
64
|
+
global.Candy.setMock('server', 'SSL', mockSSL)
|
|
65
|
+
global.Candy.setMock('server', 'Api', mockApi)
|
|
66
|
+
|
|
67
|
+
// Mock the __ function to return the key with placeholders replaced correctly
|
|
68
|
+
global.__ = jest.fn((key, ...args) => {
|
|
69
|
+
let result = key
|
|
70
|
+
// Replace numbered placeholders first (%s1, %s2, etc.)
|
|
71
|
+
args.forEach((arg, index) => {
|
|
72
|
+
result = result.replace(`%s${index + 1}`, arg)
|
|
73
|
+
})
|
|
74
|
+
// Then replace any remaining %s with the first argument
|
|
75
|
+
if (args.length > 0) {
|
|
76
|
+
result = result.replace(/%s/g, args[0])
|
|
77
|
+
}
|
|
78
|
+
return Promise.resolve(result)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Load the Subdomain module
|
|
82
|
+
Subdomain = require('../../server/src/Subdomain')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
cleanupGlobalMocks()
|
|
87
|
+
jest.resetModules()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('create', () => {
|
|
91
|
+
describe('subdomain validation and parsing', () => {
|
|
92
|
+
it('should reject invalid subdomain names with less than 3 parts', async () => {
|
|
93
|
+
const result = await Subdomain.create('invalid')
|
|
94
|
+
|
|
95
|
+
expect(result.success).toBe(false)
|
|
96
|
+
expect(result.data).toBe('Invalid subdomain name.')
|
|
97
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Invalid subdomain name.')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should reject subdomain names with only 2 parts', async () => {
|
|
101
|
+
const result = await Subdomain.create('sub.domain')
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(false)
|
|
104
|
+
expect(result.data).toBe('Invalid subdomain name.')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should reject when trying to create a subdomain for a domain that already exists as a full domain', async () => {
|
|
108
|
+
// This test should pass a 3-part domain that matches an existing domain
|
|
109
|
+
const result = await Subdomain.create('www.example.com')
|
|
110
|
+
|
|
111
|
+
expect(result.success).toBe(false)
|
|
112
|
+
expect(result.data).toBe('Domain www.example.com already exists.')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should reject when parent domain is not found', async () => {
|
|
116
|
+
const result = await Subdomain.create('sub.nonexistent.com')
|
|
117
|
+
|
|
118
|
+
expect(result.success).toBe(false)
|
|
119
|
+
expect(result.data).toBe('Domain nonexistent.com not found.')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should correctly parse multi-level subdomains', async () => {
|
|
123
|
+
const result = await Subdomain.create('api.v1.example.com')
|
|
124
|
+
|
|
125
|
+
expect(result.success).toBe(true)
|
|
126
|
+
expect(mockDNS.record).toHaveBeenCalledWith(
|
|
127
|
+
{name: 'api.v1.example.com', type: 'A'},
|
|
128
|
+
{name: 'www.api.v1.example.com', type: 'CNAME'},
|
|
129
|
+
{name: 'api.v1.example.com', type: 'MX'}
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('DNS record creation', () => {
|
|
135
|
+
it('should create A, CNAME, and MX records for new subdomain', async () => {
|
|
136
|
+
const result = await Subdomain.create('api.example.com')
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBe(true)
|
|
139
|
+
expect(mockDNS.record).toHaveBeenCalledWith(
|
|
140
|
+
{name: 'api.example.com', type: 'A'},
|
|
141
|
+
{name: 'www.api.example.com', type: 'CNAME'},
|
|
142
|
+
{name: 'api.example.com', type: 'MX'}
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should create DNS records for complex subdomain names', async () => {
|
|
147
|
+
const result = await Subdomain.create('staging.api.example.com')
|
|
148
|
+
|
|
149
|
+
expect(result.success).toBe(true)
|
|
150
|
+
expect(mockDNS.record).toHaveBeenCalledWith(
|
|
151
|
+
{name: 'staging.api.example.com', type: 'A'},
|
|
152
|
+
{name: 'www.staging.api.example.com', type: 'CNAME'},
|
|
153
|
+
{name: 'staging.api.example.com', type: 'MX'}
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('website configuration updates', () => {
|
|
159
|
+
it('should add subdomain to website configuration', async () => {
|
|
160
|
+
const result = await Subdomain.create('api.example.com')
|
|
161
|
+
|
|
162
|
+
expect(result.success).toBe(true)
|
|
163
|
+
expect(mockConfig.websites['example.com'].subdomain).toContain('api')
|
|
164
|
+
expect(mockConfig.websites['example.com'].subdomain).toContain('www.api')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should maintain sorted subdomain list', async () => {
|
|
168
|
+
await Subdomain.create('api.example.com')
|
|
169
|
+
await Subdomain.create('blog.example.com')
|
|
170
|
+
|
|
171
|
+
const subdomains = mockConfig.websites['example.com'].subdomain
|
|
172
|
+
const sortedSubdomains = [...subdomains].sort()
|
|
173
|
+
expect(subdomains).toEqual(sortedSubdomains)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should reject creation of existing subdomain', async () => {
|
|
177
|
+
// First creation should succeed
|
|
178
|
+
const result1 = await Subdomain.create('api.example.com')
|
|
179
|
+
expect(result1.success).toBe(true)
|
|
180
|
+
|
|
181
|
+
// Second creation should fail
|
|
182
|
+
const result2 = await Subdomain.create('api.example.com')
|
|
183
|
+
expect(result2.success).toBe(false)
|
|
184
|
+
expect(result2.data).toBe('Subdomain api.example.com already exists.')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should handle www prefix correctly when checking for existing subdomains', async () => {
|
|
188
|
+
// Create a subdomain
|
|
189
|
+
await Subdomain.create('api.example.com')
|
|
190
|
+
|
|
191
|
+
// Try to create www.api which should already exist
|
|
192
|
+
const result = await Subdomain.create('www.api.example.com')
|
|
193
|
+
expect(result.success).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('SSL certificate renewal triggers', () => {
|
|
198
|
+
it('should trigger SSL renewal for parent domain after subdomain creation', async () => {
|
|
199
|
+
const result = await Subdomain.create('api.example.com')
|
|
200
|
+
|
|
201
|
+
expect(result.success).toBe(true)
|
|
202
|
+
expect(mockSSL.renew).toHaveBeenCalledWith('example.com')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should trigger SSL renewal for correct parent domain in multi-level scenarios', async () => {
|
|
206
|
+
const result = await Subdomain.create('staging.api.example.com')
|
|
207
|
+
|
|
208
|
+
expect(result.success).toBe(true)
|
|
209
|
+
expect(mockSSL.renew).toHaveBeenCalledWith('example.com')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('success scenarios', () => {
|
|
214
|
+
it('should return success message with subdomain and domain names', async () => {
|
|
215
|
+
const result = await Subdomain.create('api.example.com')
|
|
216
|
+
|
|
217
|
+
expect(result.success).toBe(true)
|
|
218
|
+
expect(result.data).toBe('Subdomain api.example.com created successfully for domain example.com.')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should handle trimmed input correctly', async () => {
|
|
222
|
+
// Due to a bug in the original code, trimming doesn't work as expected
|
|
223
|
+
// The domain variable is split before trimming, so spaces cause issues
|
|
224
|
+
const result = await Subdomain.create(' api.example.com ')
|
|
225
|
+
|
|
226
|
+
// This will fail because the domain parsing happens before trimming
|
|
227
|
+
expect(result.success).toBe(false)
|
|
228
|
+
expect(result.data).toBe('Domain example.com not found.')
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('delete', () => {
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
// Add some existing subdomains for deletion tests
|
|
236
|
+
mockConfig.websites['example.com'].subdomain = ['www', 'api', 'www.api', 'blog', 'www.blog']
|
|
237
|
+
mockConfig.websites['test.org'].subdomain = ['www', 'mail', 'www.mail', 'admin', 'www.admin']
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('subdomain validation and error handling', () => {
|
|
241
|
+
it('should reject invalid subdomain names with less than 3 parts', async () => {
|
|
242
|
+
const result = await Subdomain.delete('invalid')
|
|
243
|
+
|
|
244
|
+
expect(result.success).toBe(false)
|
|
245
|
+
expect(result.data).toBe('Invalid subdomain name.')
|
|
246
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Invalid subdomain name.')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should reject deletion of a full domain', async () => {
|
|
250
|
+
// This should be a 3-part domain that matches an existing domain
|
|
251
|
+
const result = await Subdomain.delete('www.example.com')
|
|
252
|
+
|
|
253
|
+
expect(result.success).toBe(false)
|
|
254
|
+
expect(result.data).toBe('www.example.com is a domain.')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should reject deletion when parent domain is not found', async () => {
|
|
258
|
+
const result = await Subdomain.delete('sub.nonexistent.com')
|
|
259
|
+
|
|
260
|
+
expect(result.success).toBe(false)
|
|
261
|
+
expect(result.data).toBe('Domain nonexistent.com not found.')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('should reject deletion of non-existent subdomain', async () => {
|
|
265
|
+
const result = await Subdomain.delete('nonexistent.example.com')
|
|
266
|
+
|
|
267
|
+
expect(result.success).toBe(false)
|
|
268
|
+
expect(result.data).toBe('Subdomain nonexistent.example.com not found.')
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('DNS cleanup operations', () => {
|
|
273
|
+
it('should delete A, CNAME, and MX records for subdomain', async () => {
|
|
274
|
+
const result = await Subdomain.delete('api.example.com')
|
|
275
|
+
|
|
276
|
+
expect(result.success).toBe(true)
|
|
277
|
+
expect(mockDNS.delete).toHaveBeenCalledWith(
|
|
278
|
+
{name: 'api.example.com', type: 'A'},
|
|
279
|
+
{name: 'www.api.example.com', type: 'CNAME'},
|
|
280
|
+
{name: 'api.example.com', type: 'MX'}
|
|
281
|
+
)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should delete DNS records for complex subdomain names', async () => {
|
|
285
|
+
// Add a complex subdomain first
|
|
286
|
+
mockConfig.websites['example.com'].subdomain.push('staging.api', 'www.staging.api')
|
|
287
|
+
|
|
288
|
+
const result = await Subdomain.delete('staging.api.example.com')
|
|
289
|
+
|
|
290
|
+
expect(result.success).toBe(true)
|
|
291
|
+
expect(mockDNS.delete).toHaveBeenCalledWith(
|
|
292
|
+
{name: 'staging.api.example.com', type: 'A'},
|
|
293
|
+
{name: 'www.staging.api.example.com', type: 'CNAME'},
|
|
294
|
+
{name: 'staging.api.example.com', type: 'MX'}
|
|
295
|
+
)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('subdomain removal from configuration', () => {
|
|
300
|
+
it('should remove subdomain and its www variant from configuration', async () => {
|
|
301
|
+
const result = await Subdomain.delete('api.example.com')
|
|
302
|
+
|
|
303
|
+
expect(result.success).toBe(true)
|
|
304
|
+
expect(mockConfig.websites['example.com'].subdomain).not.toContain('api')
|
|
305
|
+
expect(mockConfig.websites['example.com'].subdomain).not.toContain('www.api')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should preserve other subdomains when deleting one', async () => {
|
|
309
|
+
const result = await Subdomain.delete('api.example.com')
|
|
310
|
+
|
|
311
|
+
expect(result.success).toBe(true)
|
|
312
|
+
expect(mockConfig.websites['example.com'].subdomain).toContain('www')
|
|
313
|
+
expect(mockConfig.websites['example.com'].subdomain).toContain('blog')
|
|
314
|
+
expect(mockConfig.websites['example.com'].subdomain).toContain('www.blog')
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('should handle deletion of subdomain without www variant', async () => {
|
|
318
|
+
// Add a subdomain without www variant
|
|
319
|
+
mockConfig.websites['example.com'].subdomain.push('special')
|
|
320
|
+
|
|
321
|
+
const result = await Subdomain.delete('special.example.com')
|
|
322
|
+
|
|
323
|
+
expect(result.success).toBe(true)
|
|
324
|
+
expect(mockConfig.websites['example.com'].subdomain).not.toContain('special')
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
describe('success scenarios', () => {
|
|
329
|
+
it('should return success message with subdomain and domain names', async () => {
|
|
330
|
+
const result = await Subdomain.delete('api.example.com')
|
|
331
|
+
|
|
332
|
+
expect(result.success).toBe(true)
|
|
333
|
+
expect(result.data).toBe('Subdomain api.example.com deleted successfully from domain example.com.')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('should handle trimmed input correctly', async () => {
|
|
337
|
+
// Due to a bug in the original code, trimming doesn't work as expected
|
|
338
|
+
const result = await Subdomain.delete(' api.example.com ')
|
|
339
|
+
|
|
340
|
+
// This will fail because the domain parsing happens before trimming
|
|
341
|
+
expect(result.success).toBe(false)
|
|
342
|
+
expect(result.data).toBe('Domain example.com not found.')
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
describe('list', () => {
|
|
348
|
+
beforeEach(() => {
|
|
349
|
+
// Set up test subdomains
|
|
350
|
+
mockConfig.websites['example.com'].subdomain = ['www', 'api', 'www.api', 'blog', 'www.blog']
|
|
351
|
+
mockConfig.websites['test.org'].subdomain = ['www', 'mail', 'www.mail']
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('domain validation', () => {
|
|
355
|
+
it('should reject listing for non-existent domain', async () => {
|
|
356
|
+
const result = await Subdomain.list('nonexistent.com')
|
|
357
|
+
|
|
358
|
+
expect(result.success).toBe(false)
|
|
359
|
+
expect(result.data).toBe('Domain nonexistent.com not found.')
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
describe('subdomain listing with proper formatting', () => {
|
|
364
|
+
it('should list all subdomains for a domain with proper formatting', async () => {
|
|
365
|
+
const result = await Subdomain.list('example.com')
|
|
366
|
+
|
|
367
|
+
expect(result.success).toBe(true)
|
|
368
|
+
expect(result.data).toContain('Subdomains of example.com:')
|
|
369
|
+
expect(result.data).toContain('www.example.com')
|
|
370
|
+
expect(result.data).toContain('api.example.com')
|
|
371
|
+
expect(result.data).toContain('www.api.example.com')
|
|
372
|
+
expect(result.data).toContain('blog.example.com')
|
|
373
|
+
expect(result.data).toContain('www.blog.example.com')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should format subdomain list with proper indentation', async () => {
|
|
377
|
+
const result = await Subdomain.list('example.com')
|
|
378
|
+
|
|
379
|
+
expect(result.success).toBe(true)
|
|
380
|
+
// Check that subdomains are indented with 2 spaces
|
|
381
|
+
const lines = result.data.split('\n')
|
|
382
|
+
const subdomainLines = lines.slice(1) // Skip the header line
|
|
383
|
+
subdomainLines.forEach(line => {
|
|
384
|
+
expect(line).toMatch(/^ \w+/)
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('should list subdomains for different domains correctly', async () => {
|
|
389
|
+
const result = await Subdomain.list('test.org')
|
|
390
|
+
|
|
391
|
+
expect(result.success).toBe(true)
|
|
392
|
+
expect(result.data).toContain('Subdomains of test.org:')
|
|
393
|
+
expect(result.data).toContain('www.test.org')
|
|
394
|
+
expect(result.data).toContain('mail.test.org')
|
|
395
|
+
expect(result.data).toContain('www.mail.test.org')
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('should handle domains with no additional subdomains', async () => {
|
|
399
|
+
// Create a domain with only www subdomain
|
|
400
|
+
mockConfig.websites['minimal.com'] = createMockWebsiteConfig('minimal.com', {
|
|
401
|
+
subdomain: ['www']
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const result = await Subdomain.list('minimal.com')
|
|
405
|
+
|
|
406
|
+
expect(result.success).toBe(true)
|
|
407
|
+
expect(result.data).toContain('Subdomains of minimal.com:')
|
|
408
|
+
expect(result.data).toContain('www.minimal.com')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('should handle empty subdomain arrays', async () => {
|
|
412
|
+
// Create a domain with no subdomains
|
|
413
|
+
mockConfig.websites['empty.com'] = createMockWebsiteConfig('empty.com', {
|
|
414
|
+
subdomain: []
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const result = await Subdomain.list('empty.com')
|
|
418
|
+
|
|
419
|
+
expect(result.success).toBe(true)
|
|
420
|
+
expect(result.data).toBe('Subdomains of empty.com:\n ')
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
describe('domain resolution and parent domain identification', () => {
|
|
425
|
+
it('should correctly identify parent domain for listing', async () => {
|
|
426
|
+
const result = await Subdomain.list('example.com')
|
|
427
|
+
|
|
428
|
+
expect(result.success).toBe(true)
|
|
429
|
+
expect(global.__).toHaveBeenCalledWith('Subdomains of %s:', 'example.com')
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('should handle domain names with different TLDs', async () => {
|
|
433
|
+
const result = await Subdomain.list('test.org')
|
|
434
|
+
|
|
435
|
+
expect(result.success).toBe(true)
|
|
436
|
+
expect(global.__).toHaveBeenCalledWith('Subdomains of %s:', 'test.org')
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
|
|
2
|
+
// Mock dependencies
|
|
3
|
+
const mockLog = {
|
|
4
|
+
log: jest.fn(),
|
|
5
|
+
error: jest.fn(),
|
|
6
|
+
init: jest.fn().mockReturnThis()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Global config store
|
|
10
|
+
let mockConfigData = {
|
|
11
|
+
firewall: {
|
|
12
|
+
enabled: true,
|
|
13
|
+
rateLimit: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
windowMs: 1000,
|
|
16
|
+
max: 2
|
|
17
|
+
},
|
|
18
|
+
blacklist: [],
|
|
19
|
+
whitelist: []
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Mock Candy global
|
|
24
|
+
global.Candy = {
|
|
25
|
+
core: jest.fn((module) => {
|
|
26
|
+
if (module === 'Log') return mockLog
|
|
27
|
+
if (module === 'Config') return {
|
|
28
|
+
config: mockConfigData
|
|
29
|
+
}
|
|
30
|
+
return {}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const Firewall = require('../../../server/src/Web/Firewall.js')
|
|
35
|
+
|
|
36
|
+
describe('Firewall', () => {
|
|
37
|
+
let firewall
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
jest.clearAllMocks()
|
|
41
|
+
// Reset config
|
|
42
|
+
mockConfigData.firewall = {
|
|
43
|
+
enabled: true,
|
|
44
|
+
rateLimit: {
|
|
45
|
+
enabled: true,
|
|
46
|
+
windowMs: 1000,
|
|
47
|
+
max: 2
|
|
48
|
+
},
|
|
49
|
+
blacklist: [],
|
|
50
|
+
whitelist: []
|
|
51
|
+
}
|
|
52
|
+
firewall = new Firewall()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('should allow requests from normal IPs', () => {
|
|
56
|
+
const req = { socket: { remoteAddress: '127.0.0.1' }, headers: {} }
|
|
57
|
+
expect(firewall.check(req).allowed).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('should block requests from blacklisted IPs', () => {
|
|
61
|
+
firewall.addBlock('1.2.3.4')
|
|
62
|
+
const req = { socket: { remoteAddress: '1.2.3.4' }, headers: {} }
|
|
63
|
+
const result = firewall.check(req)
|
|
64
|
+
expect(result.allowed).toBe(false)
|
|
65
|
+
expect(result.reason).toBe('blacklist')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should allow requests from whitelisted IPs even if rate limited', () => {
|
|
69
|
+
// Mock rate limit config to be very strict
|
|
70
|
+
mockConfigData.firewall.rateLimit.max = 0
|
|
71
|
+
firewall = new Firewall() // reload config
|
|
72
|
+
|
|
73
|
+
firewall.addWhitelist('1.2.3.4')
|
|
74
|
+
|
|
75
|
+
const req = { socket: { remoteAddress: '1.2.3.4' }, headers: {} }
|
|
76
|
+
|
|
77
|
+
expect(firewall.check(req).allowed).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('should enforce rate limits', () => {
|
|
81
|
+
const req = { socket: { remoteAddress: '10.0.0.1' }, headers: {} }
|
|
82
|
+
|
|
83
|
+
// Config is max 2 per 1000ms
|
|
84
|
+
expect(firewall.check(req).allowed).toBe(true) // 1
|
|
85
|
+
expect(firewall.check(req).allowed).toBe(true) // 2
|
|
86
|
+
const result = firewall.check(req) // 3 - blocked
|
|
87
|
+
expect(result.allowed).toBe(false)
|
|
88
|
+
expect(result.reason).toBe('rate_limit')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('should reset rate limits after window', async () => {
|
|
92
|
+
const req = { socket: { remoteAddress: '10.0.0.2' }, headers: {} }
|
|
93
|
+
|
|
94
|
+
expect(firewall.check(req).allowed).toBe(true) // 1
|
|
95
|
+
expect(firewall.check(req).allowed).toBe(true) // 2
|
|
96
|
+
expect(firewall.check(req).allowed).toBe(false) // 3
|
|
97
|
+
|
|
98
|
+
// Wait for window to pass (1000ms)
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 1100))
|
|
100
|
+
|
|
101
|
+
expect(firewall.check(req).allowed).toBe(true) // Should be allowed again
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('should handle IPv6 mapped IPv4 addresses', () => {
|
|
105
|
+
const req = { socket: { remoteAddress: '::ffff:127.0.0.1' }, headers: {} }
|
|
106
|
+
expect(firewall.check(req).allowed).toBe(true)
|
|
107
|
+
|
|
108
|
+
firewall.addBlock('127.0.0.1')
|
|
109
|
+
expect(firewall.check(req).allowed).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('should use x-forwarded-for if socket address is missing', () => {
|
|
113
|
+
const req = { socket: {}, headers: { 'x-forwarded-for': '1.2.3.4' } }
|
|
114
|
+
firewall.addBlock('1.2.3.4')
|
|
115
|
+
expect(firewall.check(req).allowed).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('should handle x-forwarded-for with multiple IPs', () => {
|
|
119
|
+
// First IP is client
|
|
120
|
+
const req = { socket: {}, headers: { 'x-forwarded-for': '1.2.3.4, 5.6.7.8' } }
|
|
121
|
+
firewall.addBlock('1.2.3.4')
|
|
122
|
+
const result = firewall.check(req)
|
|
123
|
+
expect(result.allowed).toBe(false)
|
|
124
|
+
expect(result.reason).toBe('blacklist')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('should handle x-forwarded-for with spaces', () => {
|
|
128
|
+
const req = { socket: {}, headers: { 'x-forwarded-for': ' 1.2.3.4 , 5.6.7.8 ' } }
|
|
129
|
+
firewall.addBlock('1.2.3.4')
|
|
130
|
+
expect(firewall.check(req).allowed).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('should allow everything when disabled', () => {
|
|
134
|
+
mockConfigData.firewall.enabled = false
|
|
135
|
+
firewall = new Firewall() // reload config
|
|
136
|
+
|
|
137
|
+
firewall.addBlock('1.2.3.4') // even if blocked
|
|
138
|
+
const req = { socket: { remoteAddress: '1.2.3.4' }, headers: {} }
|
|
139
|
+
|
|
140
|
+
expect(firewall.check(req).allowed).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('should remove block', () => {
|
|
144
|
+
firewall.addBlock('1.2.3.4')
|
|
145
|
+
const req = { socket: { remoteAddress: '1.2.3.4' }, headers: {} }
|
|
146
|
+
expect(firewall.check(req).allowed).toBe(false)
|
|
147
|
+
|
|
148
|
+
firewall.removeBlock('1.2.3.4')
|
|
149
|
+
expect(firewall.check(req).allowed).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('should remove whitelist', () => {
|
|
153
|
+
// Set strict rate limit
|
|
154
|
+
mockConfigData.firewall.rateLimit.max = 0
|
|
155
|
+
firewall = new Firewall()
|
|
156
|
+
|
|
157
|
+
firewall.addWhitelist('1.2.3.4')
|
|
158
|
+
const req = { socket: { remoteAddress: '1.2.3.4' }, headers: {} }
|
|
159
|
+
expect(firewall.check(req).allowed).toBe(true)
|
|
160
|
+
|
|
161
|
+
firewall.removeWhitelist('1.2.3.4')
|
|
162
|
+
expect(firewall.check(req).allowed).toBe(false) // rate limited
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('should persist changes to config', () => {
|
|
166
|
+
firewall.addBlock('1.1.1.1')
|
|
167
|
+
expect(mockConfigData.firewall.blacklist).toContain('1.1.1.1')
|
|
168
|
+
|
|
169
|
+
firewall.addWhitelist('2.2.2.2')
|
|
170
|
+
expect(mockConfigData.firewall.whitelist).toContain('2.2.2.2')
|
|
171
|
+
|
|
172
|
+
firewall.removeBlock('1.1.1.1')
|
|
173
|
+
expect(mockConfigData.firewall.blacklist).not.toContain('1.1.1.1')
|
|
174
|
+
})
|
|
175
|
+
})
|