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,2084 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive unit tests for the DNS.js module
|
|
3
|
+
* Tests DNS server functionality, record management, and query processing
|
|
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('DNS Module', () => {
|
|
14
|
+
let DNS
|
|
15
|
+
let mockConfig
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
setupGlobalMocks()
|
|
19
|
+
|
|
20
|
+
// Set up the Log mock before requiring DNS
|
|
21
|
+
const {mockCandy} = require('./__mocks__/globalCandy')
|
|
22
|
+
mockCandy.setMock('core', 'Log', {
|
|
23
|
+
init: jest.fn().mockReturnValue({
|
|
24
|
+
log: mockLog,
|
|
25
|
+
error: mockError
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Mock native-dns module
|
|
30
|
+
jest.doMock('native-dns', () => ({
|
|
31
|
+
createServer: jest.fn(() => ({
|
|
32
|
+
on: jest.fn(),
|
|
33
|
+
serve: jest.fn()
|
|
34
|
+
})),
|
|
35
|
+
createTCPServer: jest.fn(() => ({
|
|
36
|
+
on: jest.fn(),
|
|
37
|
+
serve: jest.fn()
|
|
38
|
+
})),
|
|
39
|
+
consts: {
|
|
40
|
+
NAME_TO_QTYPE: {
|
|
41
|
+
A: 1,
|
|
42
|
+
AAAA: 28,
|
|
43
|
+
CNAME: 5,
|
|
44
|
+
MX: 15,
|
|
45
|
+
TXT: 16,
|
|
46
|
+
NS: 2,
|
|
47
|
+
SOA: 6
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
A: jest.fn(data => ({type: 'A', ...data})),
|
|
51
|
+
AAAA: jest.fn(data => ({type: 'AAAA', ...data})),
|
|
52
|
+
CNAME: jest.fn(data => ({type: 'CNAME', ...data})),
|
|
53
|
+
MX: jest.fn(data => ({type: 'MX', ...data})),
|
|
54
|
+
TXT: jest.fn(data => ({type: 'TXT', ...data})),
|
|
55
|
+
NS: jest.fn(data => ({type: 'NS', ...data})),
|
|
56
|
+
SOA: jest.fn(data => ({type: 'SOA', ...data}))
|
|
57
|
+
}))
|
|
58
|
+
|
|
59
|
+
// Mock axios module
|
|
60
|
+
jest.doMock('axios', () => ({
|
|
61
|
+
get: jest.fn().mockResolvedValue({data: '127.0.0.1'})
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
// Setup mock config with websites
|
|
65
|
+
mockConfig = {
|
|
66
|
+
config: {
|
|
67
|
+
websites: {
|
|
68
|
+
'example.com': createMockWebsiteConfig('example.com'),
|
|
69
|
+
'test.org': createMockWebsiteConfig('test.org')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
global.Candy.setMock('core', 'Config', mockConfig)
|
|
75
|
+
|
|
76
|
+
// Clear module cache and require DNS
|
|
77
|
+
jest.resetModules()
|
|
78
|
+
DNS = require('../../server/src/DNS')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
cleanupGlobalMocks()
|
|
83
|
+
jest.resetModules()
|
|
84
|
+
jest.dontMock('native-dns')
|
|
85
|
+
jest.dontMock('axios')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('initialization', () => {
|
|
89
|
+
it('should create UDP and TCP DNS servers on initialization', () => {
|
|
90
|
+
const dns = require('native-dns')
|
|
91
|
+
|
|
92
|
+
DNS.init()
|
|
93
|
+
|
|
94
|
+
expect(dns.createServer).toHaveBeenCalled()
|
|
95
|
+
expect(dns.createTCPServer).toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should attempt to get external IP from curlmyip.org', () => {
|
|
99
|
+
const axios = require('axios')
|
|
100
|
+
|
|
101
|
+
DNS.init()
|
|
102
|
+
|
|
103
|
+
expect(axios.get).toHaveBeenCalledWith('https://curlmyip.org/', {
|
|
104
|
+
headers: {'User-Agent': 'CandyPack-DNS/1.0'},
|
|
105
|
+
timeout: 5000
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should start DNS servers on port 53 when websites exist', async () => {
|
|
110
|
+
const dns = require('native-dns')
|
|
111
|
+
|
|
112
|
+
DNS.init()
|
|
113
|
+
|
|
114
|
+
// Wait for async initialization to complete
|
|
115
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
116
|
+
|
|
117
|
+
// The servers should be created
|
|
118
|
+
expect(dns.createServer).toHaveBeenCalled()
|
|
119
|
+
expect(dns.createTCPServer).toHaveBeenCalled()
|
|
120
|
+
|
|
121
|
+
// Note: serve() is called asynchronously after port availability check
|
|
122
|
+
// This test verifies servers are created, actual serving is tested in integration tests
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should set external IP when successfully retrieved', async () => {
|
|
126
|
+
const axios = require('axios')
|
|
127
|
+
axios.get.mockResolvedValue({data: '203.0.113.1'})
|
|
128
|
+
|
|
129
|
+
DNS.init()
|
|
130
|
+
|
|
131
|
+
// Wait for the axios promise to resolve
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
133
|
+
|
|
134
|
+
expect(DNS.ip).toBe('203.0.113.1')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should handle invalid IP format from external service', async () => {
|
|
138
|
+
const axios = require('axios')
|
|
139
|
+
// mockLog already defined at top
|
|
140
|
+
axios.get.mockResolvedValue({data: 'invalid-ip-format'})
|
|
141
|
+
|
|
142
|
+
DNS.init()
|
|
143
|
+
|
|
144
|
+
// Wait for the axios promise to resolve
|
|
145
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
146
|
+
|
|
147
|
+
// Should fallback to local network IP (not 127.0.0.1 and not the invalid format)
|
|
148
|
+
expect(DNS.ip).not.toBe('127.0.0.1')
|
|
149
|
+
expect(DNS.ip).not.toBe('invalid-ip-format')
|
|
150
|
+
expect(DNS.ip).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should handle external IP detection failure', async () => {
|
|
154
|
+
const axios = require('axios')
|
|
155
|
+
// mockLog already defined at top
|
|
156
|
+
const networkError = new Error('Network timeout')
|
|
157
|
+
axios.get.mockRejectedValue(networkError)
|
|
158
|
+
|
|
159
|
+
DNS.init()
|
|
160
|
+
|
|
161
|
+
// Wait for the axios promise to resolve
|
|
162
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
163
|
+
|
|
164
|
+
// Should fallback to local network IP (not 127.0.0.1)
|
|
165
|
+
expect(DNS.ip).not.toBe('127.0.0.1')
|
|
166
|
+
expect(DNS.ip).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should handle DNS server startup errors gracefully', async () => {
|
|
170
|
+
const dns = require('native-dns')
|
|
171
|
+
// mockLog already defined at top
|
|
172
|
+
const udpServer = {
|
|
173
|
+
on: jest.fn(),
|
|
174
|
+
serve: jest.fn(() => {
|
|
175
|
+
throw new Error('Port 53 already in use')
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
const tcpServer = {
|
|
179
|
+
on: jest.fn(),
|
|
180
|
+
serve: jest.fn(() => {
|
|
181
|
+
throw new Error('Port 53 already in use')
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
dns.createServer.mockReturnValue(udpServer)
|
|
186
|
+
dns.createTCPServer.mockReturnValue(tcpServer)
|
|
187
|
+
|
|
188
|
+
DNS.init()
|
|
189
|
+
|
|
190
|
+
// Wait for async initialization
|
|
191
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
192
|
+
|
|
193
|
+
// Server creation should still happen even if serve fails
|
|
194
|
+
expect(dns.createServer).toHaveBeenCalled()
|
|
195
|
+
expect(dns.createTCPServer).toHaveBeenCalled()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should set up error handlers for UDP and TCP servers', async () => {
|
|
199
|
+
const dns = require('native-dns')
|
|
200
|
+
const udpServer = {on: jest.fn(), serve: jest.fn()}
|
|
201
|
+
const tcpServer = {on: jest.fn(), serve: jest.fn()}
|
|
202
|
+
|
|
203
|
+
dns.createServer.mockReturnValue(udpServer)
|
|
204
|
+
dns.createTCPServer.mockReturnValue(tcpServer)
|
|
205
|
+
|
|
206
|
+
DNS.init()
|
|
207
|
+
|
|
208
|
+
// Wait for async initialization
|
|
209
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
210
|
+
|
|
211
|
+
// Verify request handlers are set up (error handlers are set up during attemptDNSStart)
|
|
212
|
+
expect(udpServer.on).toHaveBeenCalledWith('request', expect.any(Function))
|
|
213
|
+
expect(tcpServer.on).toHaveBeenCalledWith('request', expect.any(Function))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should log DNS server errors when they occur', async () => {
|
|
217
|
+
const dns = require('native-dns')
|
|
218
|
+
const mockLog = global.Candy.server('Log').init('DNS').error
|
|
219
|
+
const udpServer = {on: jest.fn(), serve: jest.fn()}
|
|
220
|
+
const tcpServer = {on: jest.fn(), serve: jest.fn()}
|
|
221
|
+
|
|
222
|
+
dns.createServer.mockReturnValue(udpServer)
|
|
223
|
+
dns.createTCPServer.mockReturnValue(tcpServer)
|
|
224
|
+
|
|
225
|
+
DNS.init()
|
|
226
|
+
|
|
227
|
+
// Wait for async initialization
|
|
228
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
229
|
+
|
|
230
|
+
// Get the request handler functions (error handlers are set during attemptDNSStart)
|
|
231
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')?.[1]
|
|
232
|
+
|
|
233
|
+
// Verify request handler exists
|
|
234
|
+
expect(requestHandler).toBeDefined()
|
|
235
|
+
expect(typeof requestHandler).toBe('function')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should not start servers when no websites are configured', () => {
|
|
239
|
+
const dns = require('native-dns')
|
|
240
|
+
const udpServer = {on: jest.fn(), serve: jest.fn()}
|
|
241
|
+
const tcpServer = {on: jest.fn(), serve: jest.fn()}
|
|
242
|
+
|
|
243
|
+
dns.createServer.mockReturnValue(udpServer)
|
|
244
|
+
dns.createTCPServer.mockReturnValue(tcpServer)
|
|
245
|
+
|
|
246
|
+
// Clear websites config
|
|
247
|
+
mockConfig.config.websites = {}
|
|
248
|
+
|
|
249
|
+
DNS.init()
|
|
250
|
+
|
|
251
|
+
expect(udpServer.serve).not.toHaveBeenCalled()
|
|
252
|
+
expect(tcpServer.serve).not.toHaveBeenCalled()
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('DNS record management', () => {
|
|
257
|
+
it('should add A record to website configuration', () => {
|
|
258
|
+
const record = {name: 'example.com', type: 'A', value: '192.168.1.1'}
|
|
259
|
+
|
|
260
|
+
DNS.record(record)
|
|
261
|
+
|
|
262
|
+
expect(mockConfig.config.websites['example.com'].DNS.A).toContainEqual({
|
|
263
|
+
name: 'example.com',
|
|
264
|
+
value: '192.168.1.1'
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should add multiple record types to website configuration', () => {
|
|
269
|
+
const records = [
|
|
270
|
+
{name: 'example.com', type: 'A', value: '192.168.1.1'},
|
|
271
|
+
{name: 'example.com', type: 'MX', value: 'mail.example.com', priority: 10},
|
|
272
|
+
{name: 'example.com', type: 'TXT', value: 'v=spf1 mx ~all'}
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
DNS.record(...records)
|
|
276
|
+
|
|
277
|
+
expect(mockConfig.config.websites['example.com'].DNS.A).toContainEqual({
|
|
278
|
+
name: 'example.com',
|
|
279
|
+
value: '192.168.1.1'
|
|
280
|
+
})
|
|
281
|
+
expect(mockConfig.config.websites['example.com'].DNS.MX).toContainEqual({
|
|
282
|
+
name: 'example.com',
|
|
283
|
+
value: 'mail.example.com',
|
|
284
|
+
priority: 10
|
|
285
|
+
})
|
|
286
|
+
expect(mockConfig.config.websites['example.com'].DNS.TXT).toContainEqual({
|
|
287
|
+
name: 'example.com',
|
|
288
|
+
value: 'v=spf1 mx ~all'
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should handle subdomain records by finding parent domain', () => {
|
|
293
|
+
const record = {name: 'www.example.com', type: 'A', value: '192.168.1.1'}
|
|
294
|
+
|
|
295
|
+
DNS.record(record)
|
|
296
|
+
|
|
297
|
+
expect(mockConfig.config.websites['example.com'].DNS.A).toContainEqual({
|
|
298
|
+
name: 'www.example.com',
|
|
299
|
+
value: '192.168.1.1'
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('should automatically generate SOA record with current date serial', () => {
|
|
304
|
+
const record = {name: 'example.com', type: 'A', value: '192.168.1.1'}
|
|
305
|
+
|
|
306
|
+
DNS.record(record)
|
|
307
|
+
|
|
308
|
+
const soaRecords = mockConfig.config.websites['example.com'].DNS.SOA
|
|
309
|
+
expect(soaRecords).toHaveLength(1)
|
|
310
|
+
expect(soaRecords[0].name).toBe('example.com')
|
|
311
|
+
expect(soaRecords[0].value).toMatch(/^ns1\.example\.com hostmaster\.example\.com \d{10} 3600 600 604800 3600$/)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should delete DNS records by name and type', () => {
|
|
315
|
+
// Add a record first
|
|
316
|
+
DNS.record({name: 'example.com', type: 'A', value: '192.168.1.1'})
|
|
317
|
+
|
|
318
|
+
// Delete the record
|
|
319
|
+
DNS.delete({name: 'example.com', type: 'A'})
|
|
320
|
+
|
|
321
|
+
const aRecords = mockConfig.config.websites['example.com'].DNS.A
|
|
322
|
+
const exampleRecords = aRecords.filter(r => r.name === 'example.com' && r.value === '192.168.1.1')
|
|
323
|
+
expect(exampleRecords).toHaveLength(0)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('should delete DNS records by name, type, and value', () => {
|
|
327
|
+
// Add multiple records with same name
|
|
328
|
+
DNS.record(
|
|
329
|
+
{name: 'example.com', type: 'A', value: '192.168.1.1', unique: false},
|
|
330
|
+
{name: 'example.com', type: 'A', value: '192.168.1.2', unique: false}
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
// Delete only one specific record
|
|
334
|
+
DNS.delete({name: 'example.com', type: 'A', value: '192.168.1.1'})
|
|
335
|
+
|
|
336
|
+
const aRecords = mockConfig.config.websites['example.com'].DNS.A
|
|
337
|
+
const remainingRecords = aRecords.filter(r => r.name === 'example.com' && r.value !== '127.0.0.1')
|
|
338
|
+
expect(remainingRecords).toHaveLength(1)
|
|
339
|
+
expect(remainingRecords[0].value).toBe('192.168.1.2')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('should ignore records for non-existent domains', () => {
|
|
343
|
+
const record = {name: 'nonexistent.com', type: 'A', value: '192.168.1.1'}
|
|
344
|
+
|
|
345
|
+
DNS.record(record)
|
|
346
|
+
|
|
347
|
+
expect(mockConfig.config.websites).not.toHaveProperty('nonexistent.com')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should replace existing unique records by default', () => {
|
|
351
|
+
// Add initial record
|
|
352
|
+
DNS.record({name: 'example.com', type: 'A', value: '192.168.1.1'})
|
|
353
|
+
|
|
354
|
+
// Add another record with same name (should replace)
|
|
355
|
+
DNS.record({name: 'example.com', type: 'A', value: '192.168.1.2'})
|
|
356
|
+
|
|
357
|
+
const aRecords = mockConfig.config.websites['example.com'].DNS.A
|
|
358
|
+
const exampleRecords = aRecords.filter(r => r.name === 'example.com' && r.value !== '127.0.0.1')
|
|
359
|
+
expect(exampleRecords).toHaveLength(1)
|
|
360
|
+
expect(exampleRecords[0].value).toBe('192.168.1.2')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('should allow multiple records when unique is false', () => {
|
|
364
|
+
DNS.record(
|
|
365
|
+
{name: 'example.com', type: 'A', value: '192.168.1.1', unique: false},
|
|
366
|
+
{name: 'example.com', type: 'A', value: '192.168.1.2', unique: false}
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
const aRecords = mockConfig.config.websites['example.com'].DNS.A
|
|
370
|
+
const exampleRecords = aRecords.filter(r => r.name === 'example.com' && r.value !== '127.0.0.1')
|
|
371
|
+
expect(exampleRecords).toHaveLength(2)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('should handle all supported DNS record types', () => {
|
|
375
|
+
const records = [
|
|
376
|
+
{name: 'example.com', type: 'A', value: '192.168.1.1'},
|
|
377
|
+
{name: 'example.com', type: 'AAAA', value: '2001:db8::1'},
|
|
378
|
+
{name: 'www.example.com', type: 'CNAME', value: 'example.com'},
|
|
379
|
+
{name: 'example.com', type: 'MX', value: 'mail.example.com', priority: 10},
|
|
380
|
+
{name: 'example.com', type: 'TXT', value: 'v=spf1 mx ~all'},
|
|
381
|
+
{name: 'example.com', type: 'NS', value: 'ns1.example.com'}
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
DNS.record(...records)
|
|
385
|
+
|
|
386
|
+
const dnsConfig = mockConfig.config.websites['example.com'].DNS
|
|
387
|
+
expect(dnsConfig.A).toContainEqual({name: 'example.com', value: '192.168.1.1'})
|
|
388
|
+
expect(dnsConfig.AAAA).toContainEqual({name: 'example.com', value: '2001:db8::1'})
|
|
389
|
+
expect(dnsConfig.CNAME).toContainEqual({name: 'www.example.com', value: 'example.com'})
|
|
390
|
+
expect(dnsConfig.MX).toContainEqual({name: 'example.com', value: 'mail.example.com', priority: 10})
|
|
391
|
+
expect(dnsConfig.TXT).toContainEqual({name: 'example.com', value: 'v=spf1 mx ~all'})
|
|
392
|
+
expect(dnsConfig.NS).toContainEqual({name: 'example.com', value: 'ns1.example.com'})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('should ignore unsupported DNS record types', () => {
|
|
396
|
+
const record = {name: 'example.com', type: 'INVALID', value: 'test'}
|
|
397
|
+
|
|
398
|
+
DNS.record(record)
|
|
399
|
+
|
|
400
|
+
const dnsConfig = mockConfig.config.websites['example.com'].DNS
|
|
401
|
+
expect(dnsConfig.INVALID).toBeUndefined()
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('should ignore records without type specified', () => {
|
|
405
|
+
const record = {name: 'example.com', value: '192.168.1.1'}
|
|
406
|
+
|
|
407
|
+
DNS.record(record)
|
|
408
|
+
|
|
409
|
+
// Should not add any new records beyond the existing ones
|
|
410
|
+
const dnsConfig = mockConfig.config.websites['example.com'].DNS
|
|
411
|
+
const aRecords = dnsConfig.A.filter(r => r.value === '192.168.1.1')
|
|
412
|
+
expect(aRecords).toHaveLength(0)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('should initialize DNS object if it does not exist', () => {
|
|
416
|
+
// Remove DNS config
|
|
417
|
+
delete mockConfig.config.websites['example.com'].DNS
|
|
418
|
+
|
|
419
|
+
const record = {name: 'example.com', type: 'A', value: '192.168.1.1'}
|
|
420
|
+
DNS.record(record)
|
|
421
|
+
|
|
422
|
+
expect(mockConfig.config.websites['example.com'].DNS).toBeDefined()
|
|
423
|
+
expect(mockConfig.config.websites['example.com'].DNS.A).toContainEqual({
|
|
424
|
+
name: 'example.com',
|
|
425
|
+
value: '192.168.1.1'
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should initialize record type array if it does not exist', () => {
|
|
430
|
+
// Remove A records
|
|
431
|
+
delete mockConfig.config.websites['example.com'].DNS.A
|
|
432
|
+
|
|
433
|
+
const record = {name: 'example.com', type: 'A', value: '192.168.1.1'}
|
|
434
|
+
DNS.record(record)
|
|
435
|
+
|
|
436
|
+
expect(mockConfig.config.websites['example.com'].DNS.A).toBeDefined()
|
|
437
|
+
expect(mockConfig.config.websites['example.com'].DNS.A).toContainEqual({
|
|
438
|
+
name: 'example.com',
|
|
439
|
+
value: '192.168.1.1'
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('should generate SOA record with correct date serial format', () => {
|
|
444
|
+
const record = {name: 'example.com', type: 'A', value: '192.168.1.1'}
|
|
445
|
+
|
|
446
|
+
DNS.record(record)
|
|
447
|
+
|
|
448
|
+
const soaRecords = mockConfig.config.websites['example.com'].DNS.SOA
|
|
449
|
+
expect(soaRecords).toHaveLength(1)
|
|
450
|
+
expect(soaRecords[0].name).toBe('example.com')
|
|
451
|
+
|
|
452
|
+
// Check SOA record format: ns1.domain hostmaster.domain YYYYMMDDNN 3600 600 604800 3600
|
|
453
|
+
const soaValue = soaRecords[0].value
|
|
454
|
+
const parts = soaValue.split(' ')
|
|
455
|
+
expect(parts).toHaveLength(7)
|
|
456
|
+
expect(parts[0]).toBe('ns1.example.com')
|
|
457
|
+
expect(parts[1]).toBe('hostmaster.example.com')
|
|
458
|
+
expect(parts[2]).toMatch(/^\d{10}$/) // Date serial should be 10 digits
|
|
459
|
+
expect(parts[3]).toBe('3600')
|
|
460
|
+
expect(parts[4]).toBe('600')
|
|
461
|
+
expect(parts[5]).toBe('604800')
|
|
462
|
+
expect(parts[6]).toBe('3600')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('should update SOA record for multiple domains', () => {
|
|
466
|
+
const records = [
|
|
467
|
+
{name: 'example.com', type: 'A', value: '192.168.1.1'},
|
|
468
|
+
{name: 'test.org', type: 'A', value: '192.168.1.2'}
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
DNS.record(...records)
|
|
472
|
+
|
|
473
|
+
expect(mockConfig.config.websites['example.com'].DNS.SOA).toHaveLength(1)
|
|
474
|
+
expect(mockConfig.config.websites['test.org'].DNS.SOA).toHaveLength(1)
|
|
475
|
+
expect(mockConfig.config.websites['example.com'].DNS.SOA[0].name).toBe('example.com')
|
|
476
|
+
expect(mockConfig.config.websites['test.org'].DNS.SOA[0].name).toBe('test.org')
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it('should delete records by type only', () => {
|
|
480
|
+
// Add multiple A records
|
|
481
|
+
DNS.record(
|
|
482
|
+
{name: 'example.com', type: 'A', value: '192.168.1.1', unique: false},
|
|
483
|
+
{name: 'www.example.com', type: 'A', value: '192.168.1.2', unique: false}
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
// Delete all A records for example.com
|
|
487
|
+
DNS.delete({name: 'example.com', type: 'A'})
|
|
488
|
+
|
|
489
|
+
const aRecords = mockConfig.config.websites['example.com'].DNS.A
|
|
490
|
+
const exampleRecords = aRecords.filter(r => r.name === 'example.com' && r.value !== '127.0.0.1')
|
|
491
|
+
expect(exampleRecords).toHaveLength(0)
|
|
492
|
+
|
|
493
|
+
// www.example.com record should still exist
|
|
494
|
+
const wwwRecords = aRecords.filter(r => r.name === 'www.example.com')
|
|
495
|
+
expect(wwwRecords).toHaveLength(1)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
it('should handle deletion of non-existent records gracefully', () => {
|
|
499
|
+
DNS.delete({name: 'nonexistent.com', type: 'A'})
|
|
500
|
+
DNS.delete({name: 'example.com', type: 'NONEXISTENT'})
|
|
501
|
+
|
|
502
|
+
// Should not throw errors and existing records should remain
|
|
503
|
+
const aRecords = mockConfig.config.websites['example.com'].DNS.A
|
|
504
|
+
expect(aRecords).toBeDefined()
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('should handle deletion when DNS config does not exist', () => {
|
|
508
|
+
delete mockConfig.config.websites['example.com'].DNS
|
|
509
|
+
|
|
510
|
+
DNS.delete({name: 'example.com', type: 'A'})
|
|
511
|
+
|
|
512
|
+
// Should not throw errors
|
|
513
|
+
expect(mockConfig.config.websites['example.com'].DNS).toBeUndefined()
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('should handle deletion when record type does not exist', () => {
|
|
517
|
+
delete mockConfig.config.websites['example.com'].DNS.A
|
|
518
|
+
|
|
519
|
+
DNS.delete({name: 'example.com', type: 'A'})
|
|
520
|
+
|
|
521
|
+
// Should not throw errors
|
|
522
|
+
expect(mockConfig.config.websites['example.com'].DNS.A).toBeUndefined()
|
|
523
|
+
})
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
describe('DNS query processing', () => {
|
|
527
|
+
let mockRequest, mockResponse
|
|
528
|
+
|
|
529
|
+
beforeEach(() => {
|
|
530
|
+
// Set up mock DNS request and response objects
|
|
531
|
+
mockRequest = {
|
|
532
|
+
address: {address: '127.0.0.1'}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
mockResponse = {
|
|
536
|
+
question: [{name: 'example.com', type: 1}], // A record query
|
|
537
|
+
answer: [],
|
|
538
|
+
authority: [],
|
|
539
|
+
header: {},
|
|
540
|
+
send: jest.fn()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Initialize DNS to set up servers
|
|
544
|
+
DNS.init()
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it('should process A record queries correctly', () => {
|
|
548
|
+
const dns = require('native-dns')
|
|
549
|
+
dns.consts.NAME_TO_QTYPE.A = 1
|
|
550
|
+
|
|
551
|
+
// Add A record
|
|
552
|
+
DNS.record({name: 'example.com', type: 'A', value: '192.168.1.1'})
|
|
553
|
+
|
|
554
|
+
mockResponse.question[0] = {name: 'example.com', type: 1}
|
|
555
|
+
|
|
556
|
+
// Get the request handler
|
|
557
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
558
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
559
|
+
|
|
560
|
+
requestHandler(mockRequest, mockResponse)
|
|
561
|
+
|
|
562
|
+
expect(dns.A).toHaveBeenCalledWith({
|
|
563
|
+
name: 'example.com',
|
|
564
|
+
address: '192.168.1.1',
|
|
565
|
+
ttl: 3600
|
|
566
|
+
})
|
|
567
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('should process AAAA record queries correctly', () => {
|
|
571
|
+
const dns = require('native-dns')
|
|
572
|
+
dns.consts.NAME_TO_QTYPE.AAAA = 28
|
|
573
|
+
|
|
574
|
+
// Add AAAA record
|
|
575
|
+
DNS.record({name: 'example.com', type: 'AAAA', value: '2001:db8::1'})
|
|
576
|
+
|
|
577
|
+
mockResponse.question[0] = {name: 'example.com', type: 28}
|
|
578
|
+
|
|
579
|
+
// Get the request handler
|
|
580
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
581
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
582
|
+
|
|
583
|
+
requestHandler(mockRequest, mockResponse)
|
|
584
|
+
|
|
585
|
+
expect(dns.AAAA).toHaveBeenCalledWith({
|
|
586
|
+
name: 'example.com',
|
|
587
|
+
address: '2001:db8::1',
|
|
588
|
+
ttl: 3600
|
|
589
|
+
})
|
|
590
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('should process CNAME record queries correctly', () => {
|
|
594
|
+
const dns = require('native-dns')
|
|
595
|
+
dns.consts.NAME_TO_QTYPE.CNAME = 5
|
|
596
|
+
|
|
597
|
+
// Add CNAME record
|
|
598
|
+
DNS.record({name: 'www.example.com', type: 'CNAME', value: 'example.com'})
|
|
599
|
+
|
|
600
|
+
mockResponse.question[0] = {name: 'www.example.com', type: 5}
|
|
601
|
+
|
|
602
|
+
// Get the request handler
|
|
603
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
604
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
605
|
+
|
|
606
|
+
requestHandler(mockRequest, mockResponse)
|
|
607
|
+
|
|
608
|
+
expect(dns.CNAME).toHaveBeenCalledWith({
|
|
609
|
+
name: 'www.example.com',
|
|
610
|
+
data: 'example.com',
|
|
611
|
+
ttl: 3600
|
|
612
|
+
})
|
|
613
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('should process MX record queries correctly', () => {
|
|
617
|
+
const dns = require('native-dns')
|
|
618
|
+
dns.consts.NAME_TO_QTYPE.MX = 15
|
|
619
|
+
|
|
620
|
+
// Add MX record
|
|
621
|
+
DNS.record({name: 'example.com', type: 'MX', value: 'mail.example.com', priority: 10})
|
|
622
|
+
|
|
623
|
+
mockResponse.question[0] = {name: 'example.com', type: 15}
|
|
624
|
+
|
|
625
|
+
// Get the request handler
|
|
626
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
627
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
628
|
+
|
|
629
|
+
requestHandler(mockRequest, mockResponse)
|
|
630
|
+
|
|
631
|
+
expect(dns.MX).toHaveBeenCalledWith({
|
|
632
|
+
name: 'example.com',
|
|
633
|
+
exchange: 'mail.example.com',
|
|
634
|
+
priority: 10,
|
|
635
|
+
ttl: 3600
|
|
636
|
+
})
|
|
637
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
it('should process TXT record queries correctly', () => {
|
|
641
|
+
const dns = require('native-dns')
|
|
642
|
+
dns.consts.NAME_TO_QTYPE.TXT = 16
|
|
643
|
+
|
|
644
|
+
// Add TXT record
|
|
645
|
+
DNS.record({name: 'example.com', type: 'TXT', value: 'v=spf1 mx ~all'})
|
|
646
|
+
|
|
647
|
+
mockResponse.question[0] = {name: 'example.com', type: 16}
|
|
648
|
+
|
|
649
|
+
// Get the request handler
|
|
650
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
651
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
652
|
+
|
|
653
|
+
requestHandler(mockRequest, mockResponse)
|
|
654
|
+
|
|
655
|
+
expect(dns.TXT).toHaveBeenCalledWith({
|
|
656
|
+
name: 'example.com',
|
|
657
|
+
data: ['v=spf1 mx ~all'],
|
|
658
|
+
ttl: 3600
|
|
659
|
+
})
|
|
660
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
it('should process NS record queries correctly', () => {
|
|
664
|
+
const dns = require('native-dns')
|
|
665
|
+
dns.consts.NAME_TO_QTYPE.NS = 2
|
|
666
|
+
|
|
667
|
+
// Add NS record
|
|
668
|
+
DNS.record({name: 'example.com', type: 'NS', value: 'ns1.example.com'})
|
|
669
|
+
|
|
670
|
+
mockResponse.question[0] = {name: 'example.com', type: 2}
|
|
671
|
+
|
|
672
|
+
// Get the request handler
|
|
673
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
674
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
675
|
+
|
|
676
|
+
requestHandler(mockRequest, mockResponse)
|
|
677
|
+
|
|
678
|
+
expect(dns.NS).toHaveBeenCalledWith({
|
|
679
|
+
name: 'example.com',
|
|
680
|
+
data: 'ns1.example.com',
|
|
681
|
+
ttl: 3600
|
|
682
|
+
})
|
|
683
|
+
expect(mockResponse.header.aa).toBe(1)
|
|
684
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
it('should process SOA record queries correctly', () => {
|
|
688
|
+
const dns = require('native-dns')
|
|
689
|
+
dns.consts.NAME_TO_QTYPE.SOA = 6
|
|
690
|
+
|
|
691
|
+
// Add SOA record manually (normally auto-generated)
|
|
692
|
+
mockConfig.config.websites['example.com'].DNS.SOA = [
|
|
693
|
+
{
|
|
694
|
+
name: 'example.com',
|
|
695
|
+
value: 'ns1.example.com hostmaster.example.com 2023120101 3600 600 604800 3600'
|
|
696
|
+
}
|
|
697
|
+
]
|
|
698
|
+
|
|
699
|
+
mockResponse.question[0] = {name: 'example.com', type: 6}
|
|
700
|
+
|
|
701
|
+
// Get the request handler
|
|
702
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
703
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
704
|
+
|
|
705
|
+
requestHandler(mockRequest, mockResponse)
|
|
706
|
+
|
|
707
|
+
expect(dns.SOA).toHaveBeenCalledWith({
|
|
708
|
+
name: 'example.com',
|
|
709
|
+
primary: 'ns1.example.com',
|
|
710
|
+
admin: 'hostmaster.example.com',
|
|
711
|
+
serial: 2023120101,
|
|
712
|
+
refresh: 3600,
|
|
713
|
+
retry: 600,
|
|
714
|
+
expiration: 604800,
|
|
715
|
+
minimum: 3600,
|
|
716
|
+
ttl: 3600
|
|
717
|
+
})
|
|
718
|
+
expect(mockResponse.header.aa).toBe(1)
|
|
719
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
it('should handle queries for non-existent domains', () => {
|
|
723
|
+
mockResponse.question[0] = {name: 'nonexistent.com', type: 1}
|
|
724
|
+
|
|
725
|
+
// Get the request handler
|
|
726
|
+
const dns = require('native-dns')
|
|
727
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
728
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
729
|
+
|
|
730
|
+
requestHandler(mockRequest, mockResponse)
|
|
731
|
+
|
|
732
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
733
|
+
expect(mockResponse.answer).toHaveLength(0)
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
it('should handle queries for domains without DNS config', () => {
|
|
737
|
+
delete mockConfig.config.websites['example.com'].DNS
|
|
738
|
+
|
|
739
|
+
mockResponse.question[0] = {name: 'example.com', type: 1}
|
|
740
|
+
|
|
741
|
+
// Get the request handler
|
|
742
|
+
const dns = require('native-dns')
|
|
743
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
744
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
745
|
+
|
|
746
|
+
requestHandler(mockRequest, mockResponse)
|
|
747
|
+
|
|
748
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
749
|
+
expect(mockResponse.answer).toHaveLength(0)
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
it('should implement rate limiting per client IP', () => {
|
|
753
|
+
const dns = require('native-dns')
|
|
754
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
755
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
756
|
+
|
|
757
|
+
// Mock Date.now to control time
|
|
758
|
+
const originalDateNow = Date.now
|
|
759
|
+
let currentTime = 1000000
|
|
760
|
+
Date.now = jest.fn(() => currentTime)
|
|
761
|
+
|
|
762
|
+
// Make 101 requests (exceeding rate limit of 100)
|
|
763
|
+
for (let i = 0; i < 101; i++) {
|
|
764
|
+
const request = {address: {address: '192.168.1.100'}}
|
|
765
|
+
const response = {
|
|
766
|
+
question: [{name: 'example.com', type: 1}],
|
|
767
|
+
answer: [],
|
|
768
|
+
authority: [],
|
|
769
|
+
header: {},
|
|
770
|
+
send: jest.fn()
|
|
771
|
+
}
|
|
772
|
+
requestHandler(request, response)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// The 101st request should be rate limited (no processing)
|
|
776
|
+
expect(mockResponse.send).toHaveBeenCalledTimes(0) // Original response not used in loop
|
|
777
|
+
|
|
778
|
+
// Restore Date.now
|
|
779
|
+
Date.now = originalDateNow
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
it('should reset rate limiting after time window', () => {
|
|
783
|
+
const dns = require('native-dns')
|
|
784
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
785
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
786
|
+
|
|
787
|
+
// Mock Date.now to control time
|
|
788
|
+
const originalDateNow = Date.now
|
|
789
|
+
let currentTime = 1000000
|
|
790
|
+
Date.now = jest.fn(() => currentTime)
|
|
791
|
+
|
|
792
|
+
// Make 100 requests
|
|
793
|
+
for (let i = 0; i < 100; i++) {
|
|
794
|
+
const request = {address: {address: '192.168.1.100'}}
|
|
795
|
+
const response = {
|
|
796
|
+
question: [{name: 'example.com', type: 1}],
|
|
797
|
+
answer: [],
|
|
798
|
+
authority: [],
|
|
799
|
+
header: {},
|
|
800
|
+
send: jest.fn()
|
|
801
|
+
}
|
|
802
|
+
requestHandler(request, response)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Advance time by more than rate limit window (60 seconds)
|
|
806
|
+
currentTime += 61000
|
|
807
|
+
|
|
808
|
+
// Make another request - should be allowed
|
|
809
|
+
const request = {address: {address: '192.168.1.100'}}
|
|
810
|
+
const response = {
|
|
811
|
+
question: [{name: 'example.com', type: 1}],
|
|
812
|
+
answer: [],
|
|
813
|
+
authority: [],
|
|
814
|
+
header: {},
|
|
815
|
+
send: jest.fn()
|
|
816
|
+
}
|
|
817
|
+
requestHandler(request, response)
|
|
818
|
+
|
|
819
|
+
expect(response.send).toHaveBeenCalled()
|
|
820
|
+
|
|
821
|
+
// Restore Date.now
|
|
822
|
+
Date.now = originalDateNow
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
it('should handle malformed DNS requests gracefully', () => {
|
|
826
|
+
const dns = require('native-dns')
|
|
827
|
+
// mockLog already defined at top
|
|
828
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
829
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
830
|
+
|
|
831
|
+
// Test with null request - this will be caught by outer try-catch
|
|
832
|
+
requestHandler(null, mockResponse)
|
|
833
|
+
expect(mockLog).toHaveBeenCalledWith('Error processing DNS request from unknown')
|
|
834
|
+
|
|
835
|
+
// Test with missing question - create a proper request but invalid response
|
|
836
|
+
const invalidResponse = {send: jest.fn()}
|
|
837
|
+
requestHandler(mockRequest, invalidResponse)
|
|
838
|
+
expect(mockLog).toHaveBeenCalledWith('Invalid DNS request structure from 127.0.0.1')
|
|
839
|
+
|
|
840
|
+
// Test with empty question array
|
|
841
|
+
const emptyQuestionResponse = {question: [], send: jest.fn()}
|
|
842
|
+
requestHandler(mockRequest, emptyQuestionResponse)
|
|
843
|
+
expect(mockLog).toHaveBeenCalledWith('Invalid DNS request structure from 127.0.0.1')
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
it('should handle request processing errors gracefully', () => {
|
|
847
|
+
const dns = require('native-dns')
|
|
848
|
+
const mockLog = global.Candy.server('Log').init('DNS').error
|
|
849
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
850
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
851
|
+
|
|
852
|
+
// Mock DNS record processing to throw an error
|
|
853
|
+
dns.A.mockImplementation(() => {
|
|
854
|
+
throw new Error('DNS processing error')
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
// Add A record to trigger processing
|
|
858
|
+
DNS.record({name: 'example.com', type: 'A', value: '192.168.1.1'})
|
|
859
|
+
|
|
860
|
+
requestHandler(mockRequest, mockResponse)
|
|
861
|
+
|
|
862
|
+
expect(mockLog).toHaveBeenCalledWith('Error processing A records:', 'DNS processing error')
|
|
863
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
it('should handle case-insensitive domain names', () => {
|
|
867
|
+
const dns = require('native-dns')
|
|
868
|
+
dns.consts.NAME_TO_QTYPE.A = 1
|
|
869
|
+
|
|
870
|
+
// Add A record
|
|
871
|
+
DNS.record({name: 'example.com', type: 'A', value: '192.168.1.1'})
|
|
872
|
+
|
|
873
|
+
// Query with uppercase domain
|
|
874
|
+
mockResponse.question[0] = {name: 'EXAMPLE.COM', type: 1}
|
|
875
|
+
|
|
876
|
+
// Get the request handler
|
|
877
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
878
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
879
|
+
|
|
880
|
+
requestHandler(mockRequest, mockResponse)
|
|
881
|
+
|
|
882
|
+
expect(mockResponse.question[0].name).toBe('example.com') // Should be normalized
|
|
883
|
+
expect(dns.A).toHaveBeenCalled()
|
|
884
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
it('should process ANY queries by returning all record types', () => {
|
|
888
|
+
const dns = require('native-dns')
|
|
889
|
+
|
|
890
|
+
// Add multiple record types
|
|
891
|
+
DNS.record(
|
|
892
|
+
{name: 'example.com', type: 'A', value: '192.168.1.1'},
|
|
893
|
+
{name: 'example.com', type: 'MX', value: 'mail.example.com', priority: 10},
|
|
894
|
+
{name: 'example.com', type: 'TXT', value: 'v=spf1 mx ~all'}
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
// Query with unknown type (should process all)
|
|
898
|
+
mockResponse.question[0] = {name: 'example.com', type: 255} // ANY query
|
|
899
|
+
|
|
900
|
+
// Get the request handler
|
|
901
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
902
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
903
|
+
|
|
904
|
+
requestHandler(mockRequest, mockResponse)
|
|
905
|
+
|
|
906
|
+
expect(dns.A).toHaveBeenCalled()
|
|
907
|
+
expect(dns.MX).toHaveBeenCalled()
|
|
908
|
+
expect(dns.TXT).toHaveBeenCalled()
|
|
909
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
it('should use default values when record values are missing', () => {
|
|
913
|
+
const dns = require('native-dns')
|
|
914
|
+
dns.consts.NAME_TO_QTYPE.A = 1
|
|
915
|
+
|
|
916
|
+
// Add A record without value (should use server IP)
|
|
917
|
+
DNS.record({name: 'example.com', type: 'A'})
|
|
918
|
+
|
|
919
|
+
mockResponse.question[0] = {name: 'example.com', type: 1}
|
|
920
|
+
|
|
921
|
+
// Get the request handler
|
|
922
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
923
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
924
|
+
|
|
925
|
+
requestHandler(mockRequest, mockResponse)
|
|
926
|
+
|
|
927
|
+
expect(dns.A).toHaveBeenCalledWith({
|
|
928
|
+
name: 'example.com',
|
|
929
|
+
address: DNS.ip, // Should use server IP as default
|
|
930
|
+
ttl: 3600
|
|
931
|
+
})
|
|
932
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
it('should handle subdomain queries by finding parent domain', () => {
|
|
936
|
+
const dns = require('native-dns')
|
|
937
|
+
dns.consts.NAME_TO_QTYPE.A = 1
|
|
938
|
+
|
|
939
|
+
// Add A record for subdomain
|
|
940
|
+
DNS.record({name: 'api.example.com', type: 'A', value: '192.168.1.100'})
|
|
941
|
+
|
|
942
|
+
mockResponse.question[0] = {name: 'api.example.com', type: 1}
|
|
943
|
+
|
|
944
|
+
// Get the request handler
|
|
945
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
946
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
947
|
+
|
|
948
|
+
requestHandler(mockRequest, mockResponse)
|
|
949
|
+
|
|
950
|
+
expect(dns.A).toHaveBeenCalledWith({
|
|
951
|
+
name: 'api.example.com',
|
|
952
|
+
address: '192.168.1.100',
|
|
953
|
+
ttl: 3600
|
|
954
|
+
})
|
|
955
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
it('should handle CAA record queries correctly', () => {
|
|
959
|
+
const dns = require('native-dns')
|
|
960
|
+
dns.consts.NAME_TO_QTYPE.CAA = 257
|
|
961
|
+
|
|
962
|
+
// Mock CAA function
|
|
963
|
+
dns.CAA = jest.fn(data => ({type: 'CAA', ...data}))
|
|
964
|
+
|
|
965
|
+
// Add CAA record
|
|
966
|
+
DNS.record({name: 'example.com', type: 'CAA', value: '0 issue letsencrypt.org'})
|
|
967
|
+
|
|
968
|
+
mockResponse.question[0] = {name: 'example.com', type: 257}
|
|
969
|
+
|
|
970
|
+
// Get the request handler
|
|
971
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
972
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
973
|
+
|
|
974
|
+
requestHandler(mockRequest, mockResponse)
|
|
975
|
+
|
|
976
|
+
expect(dns.CAA).toHaveBeenCalledWith({
|
|
977
|
+
name: 'example.com',
|
|
978
|
+
flags: 0,
|
|
979
|
+
tag: 'issue',
|
|
980
|
+
value: 'letsencrypt.org',
|
|
981
|
+
ttl: 3600
|
|
982
|
+
})
|
|
983
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
it('should add default CAA records when none exist', () => {
|
|
987
|
+
const dns = require('native-dns')
|
|
988
|
+
dns.consts.NAME_TO_QTYPE.CAA = 257
|
|
989
|
+
|
|
990
|
+
// Mock CAA function
|
|
991
|
+
dns.CAA = jest.fn(data => ({type: 'CAA', ...data}))
|
|
992
|
+
|
|
993
|
+
// Initialize CAA array but leave it empty
|
|
994
|
+
mockConfig.config.websites['example.com'].DNS.CAA = []
|
|
995
|
+
|
|
996
|
+
mockResponse.question[0] = {name: 'example.com', type: 257}
|
|
997
|
+
|
|
998
|
+
// Get the request handler
|
|
999
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1000
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1001
|
+
|
|
1002
|
+
requestHandler(mockRequest, mockResponse)
|
|
1003
|
+
|
|
1004
|
+
// Should add default Let's Encrypt CAA records
|
|
1005
|
+
expect(dns.CAA).toHaveBeenCalledWith(
|
|
1006
|
+
expect.objectContaining({
|
|
1007
|
+
name: 'example.com',
|
|
1008
|
+
tag: 'issue',
|
|
1009
|
+
value: 'letsencrypt.org'
|
|
1010
|
+
})
|
|
1011
|
+
)
|
|
1012
|
+
expect(dns.CAA).toHaveBeenCalledWith(
|
|
1013
|
+
expect.objectContaining({
|
|
1014
|
+
name: 'example.com',
|
|
1015
|
+
tag: 'issuewild',
|
|
1016
|
+
value: 'letsencrypt.org'
|
|
1017
|
+
})
|
|
1018
|
+
)
|
|
1019
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
it('should handle NXDOMAIN for unknown domains', () => {
|
|
1023
|
+
const dns = require('native-dns')
|
|
1024
|
+
dns.consts.NAME_TO_RCODE = {NXDOMAIN: 3}
|
|
1025
|
+
|
|
1026
|
+
mockResponse.question[0] = {name: 'unknown.domain', type: 1}
|
|
1027
|
+
|
|
1028
|
+
// Get the request handler
|
|
1029
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1030
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1031
|
+
|
|
1032
|
+
requestHandler(mockRequest, mockResponse)
|
|
1033
|
+
|
|
1034
|
+
expect(mockResponse.header.rcode).toBe(3)
|
|
1035
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
it('should skip rate limiting for localhost', () => {
|
|
1039
|
+
const dns = require('native-dns')
|
|
1040
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1041
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1042
|
+
|
|
1043
|
+
let lastResponse
|
|
1044
|
+
// Make 150 requests from localhost (exceeds rate limit)
|
|
1045
|
+
for (let i = 0; i < 150; i++) {
|
|
1046
|
+
const request = {address: {address: '127.0.0.1'}}
|
|
1047
|
+
lastResponse = {
|
|
1048
|
+
question: [{name: 'example.com', type: 1}],
|
|
1049
|
+
answer: [],
|
|
1050
|
+
authority: [],
|
|
1051
|
+
header: {},
|
|
1052
|
+
send: jest.fn()
|
|
1053
|
+
}
|
|
1054
|
+
requestHandler(request, lastResponse)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// All requests should be processed (no rate limiting for localhost)
|
|
1058
|
+
// Last response should have been sent
|
|
1059
|
+
expect(lastResponse.send).toHaveBeenCalled()
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
it('should handle malformed CAA records gracefully', () => {
|
|
1063
|
+
const dns = require('native-dns')
|
|
1064
|
+
dns.consts.NAME_TO_QTYPE.CAA = 257
|
|
1065
|
+
dns.CAA = jest.fn(data => ({type: 'CAA', ...data}))
|
|
1066
|
+
|
|
1067
|
+
// Add malformed CAA record (missing parts)
|
|
1068
|
+
mockConfig.config.websites['example.com'].DNS.CAA = [
|
|
1069
|
+
{name: 'example.com', value: '0 issue'} // Missing value part
|
|
1070
|
+
]
|
|
1071
|
+
|
|
1072
|
+
mockResponse.question[0] = {name: 'example.com', type: 257}
|
|
1073
|
+
|
|
1074
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1075
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1076
|
+
|
|
1077
|
+
requestHandler(mockRequest, mockResponse)
|
|
1078
|
+
|
|
1079
|
+
// Should not crash, should send response
|
|
1080
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
it('should handle malformed SOA records gracefully', () => {
|
|
1084
|
+
const dns = require('native-dns')
|
|
1085
|
+
dns.consts.NAME_TO_QTYPE.SOA = 6
|
|
1086
|
+
|
|
1087
|
+
// Add malformed SOA record (not enough parts)
|
|
1088
|
+
mockConfig.config.websites['example.com'].DNS.SOA = [
|
|
1089
|
+
{name: 'example.com', value: 'ns1.example.com hostmaster.example.com'} // Missing serial and other fields
|
|
1090
|
+
]
|
|
1091
|
+
|
|
1092
|
+
mockResponse.question[0] = {name: 'example.com', type: 6}
|
|
1093
|
+
|
|
1094
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1095
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1096
|
+
|
|
1097
|
+
requestHandler(mockRequest, mockResponse)
|
|
1098
|
+
|
|
1099
|
+
// Should not crash, should send response
|
|
1100
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
it('should handle null records in TXT processing', () => {
|
|
1104
|
+
const dns = require('native-dns')
|
|
1105
|
+
dns.consts.NAME_TO_QTYPE.TXT = 16
|
|
1106
|
+
|
|
1107
|
+
// Add null record
|
|
1108
|
+
mockConfig.config.websites['example.com'].DNS.TXT = [null, {name: 'example.com', value: 'valid-txt'}]
|
|
1109
|
+
|
|
1110
|
+
mockResponse.question[0] = {name: 'example.com', type: 16}
|
|
1111
|
+
|
|
1112
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1113
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1114
|
+
|
|
1115
|
+
requestHandler(mockRequest, mockResponse)
|
|
1116
|
+
|
|
1117
|
+
// Should process valid record and skip null
|
|
1118
|
+
expect(dns.TXT).toHaveBeenCalledWith({
|
|
1119
|
+
name: 'example.com',
|
|
1120
|
+
data: ['valid-txt'],
|
|
1121
|
+
ttl: 3600
|
|
1122
|
+
})
|
|
1123
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
it('should handle null records in SOA processing', () => {
|
|
1127
|
+
const dns = require('native-dns')
|
|
1128
|
+
dns.consts.NAME_TO_QTYPE.SOA = 6
|
|
1129
|
+
|
|
1130
|
+
// Add null record
|
|
1131
|
+
mockConfig.config.websites['example.com'].DNS.SOA = [
|
|
1132
|
+
null,
|
|
1133
|
+
{name: 'example.com', value: 'ns1.example.com hostmaster.example.com 2023120101 3600 600 604800 3600'}
|
|
1134
|
+
]
|
|
1135
|
+
|
|
1136
|
+
mockResponse.question[0] = {name: 'example.com', type: 6}
|
|
1137
|
+
|
|
1138
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1139
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1140
|
+
|
|
1141
|
+
requestHandler(mockRequest, mockResponse)
|
|
1142
|
+
|
|
1143
|
+
// Should process valid record and skip null
|
|
1144
|
+
expect(dns.SOA).toHaveBeenCalled()
|
|
1145
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
1146
|
+
})
|
|
1147
|
+
|
|
1148
|
+
it('should handle null records in CAA processing', () => {
|
|
1149
|
+
const dns = require('native-dns')
|
|
1150
|
+
dns.consts.NAME_TO_QTYPE.CAA = 257
|
|
1151
|
+
dns.CAA = jest.fn(data => ({type: 'CAA', ...data}))
|
|
1152
|
+
|
|
1153
|
+
// Add null record
|
|
1154
|
+
mockConfig.config.websites['example.com'].DNS.CAA = [null, {name: 'example.com', value: '0 issue letsencrypt.org'}]
|
|
1155
|
+
|
|
1156
|
+
mockResponse.question[0] = {name: 'example.com', type: 257}
|
|
1157
|
+
|
|
1158
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1159
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1160
|
+
|
|
1161
|
+
requestHandler(mockRequest, mockResponse)
|
|
1162
|
+
|
|
1163
|
+
// Should process valid record and skip null
|
|
1164
|
+
expect(dns.CAA).toHaveBeenCalled()
|
|
1165
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
it('should handle response.send failure gracefully', () => {
|
|
1169
|
+
const dns = require('native-dns')
|
|
1170
|
+
const mockLog = global.Candy.server('Log').init('DNS').error
|
|
1171
|
+
|
|
1172
|
+
mockResponse.send = jest.fn(() => {
|
|
1173
|
+
throw new Error('Send failed')
|
|
1174
|
+
})
|
|
1175
|
+
mockResponse.question[0] = {name: 'example.com', type: 1}
|
|
1176
|
+
|
|
1177
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1178
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1179
|
+
|
|
1180
|
+
// Should not throw
|
|
1181
|
+
expect(() => requestHandler(mockRequest, mockResponse)).not.toThrow()
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
it('should handle request with unknown client IP', () => {
|
|
1185
|
+
const dns = require('native-dns')
|
|
1186
|
+
// mockLog already defined at top
|
|
1187
|
+
|
|
1188
|
+
const requestWithoutIP = {}
|
|
1189
|
+
const response = {
|
|
1190
|
+
question: [{name: 'example.com', type: 1}],
|
|
1191
|
+
answer: [],
|
|
1192
|
+
authority: [],
|
|
1193
|
+
header: {},
|
|
1194
|
+
send: jest.fn()
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1198
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1199
|
+
|
|
1200
|
+
requestHandler(requestWithoutIP, response)
|
|
1201
|
+
|
|
1202
|
+
// Should handle gracefully
|
|
1203
|
+
expect(response.send).toHaveBeenCalled()
|
|
1204
|
+
})
|
|
1205
|
+
|
|
1206
|
+
it('should handle IPv6 localhost in rate limiting', () => {
|
|
1207
|
+
const dns = require('native-dns')
|
|
1208
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1209
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1210
|
+
|
|
1211
|
+
let lastResponse
|
|
1212
|
+
// Make 150 requests from IPv6 localhost (exceeds rate limit)
|
|
1213
|
+
for (let i = 0; i < 150; i++) {
|
|
1214
|
+
const request = {address: {address: '::1'}}
|
|
1215
|
+
lastResponse = {
|
|
1216
|
+
question: [{name: 'example.com', type: 1}],
|
|
1217
|
+
answer: [],
|
|
1218
|
+
authority: [],
|
|
1219
|
+
header: {},
|
|
1220
|
+
send: jest.fn()
|
|
1221
|
+
}
|
|
1222
|
+
requestHandler(request, lastResponse)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// All requests should be processed (no rate limiting for localhost)
|
|
1226
|
+
expect(lastResponse.send).toHaveBeenCalled()
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
it('should handle custom TTL values in records', () => {
|
|
1230
|
+
const dns = require('native-dns')
|
|
1231
|
+
dns.consts.NAME_TO_QTYPE.A = 1
|
|
1232
|
+
|
|
1233
|
+
// Add A record with custom TTL
|
|
1234
|
+
DNS.record({name: 'example.com', type: 'A', value: '192.168.1.1', ttl: 7200})
|
|
1235
|
+
|
|
1236
|
+
mockResponse.question[0] = {name: 'example.com', type: 1}
|
|
1237
|
+
|
|
1238
|
+
const udpServer = dns.createServer.mock.results[0].value
|
|
1239
|
+
const requestHandler = udpServer.on.mock.calls.find(call => call[0] === 'request')[1]
|
|
1240
|
+
|
|
1241
|
+
requestHandler(mockRequest, mockResponse)
|
|
1242
|
+
|
|
1243
|
+
expect(dns.A).toHaveBeenCalledWith({
|
|
1244
|
+
name: 'example.com',
|
|
1245
|
+
address: '192.168.1.1',
|
|
1246
|
+
ttl: 7200
|
|
1247
|
+
})
|
|
1248
|
+
expect(mockResponse.send).toHaveBeenCalled()
|
|
1249
|
+
})
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
describe('advanced initialization scenarios', () => {
|
|
1253
|
+
beforeEach(() => {
|
|
1254
|
+
setupGlobalMocks()
|
|
1255
|
+
|
|
1256
|
+
jest.doMock('native-dns', () => ({
|
|
1257
|
+
createServer: jest.fn(() => ({
|
|
1258
|
+
on: jest.fn(),
|
|
1259
|
+
serve: jest.fn()
|
|
1260
|
+
})),
|
|
1261
|
+
createTCPServer: jest.fn(() => ({
|
|
1262
|
+
on: jest.fn(),
|
|
1263
|
+
serve: jest.fn()
|
|
1264
|
+
})),
|
|
1265
|
+
consts: {
|
|
1266
|
+
NAME_TO_QTYPE: {A: 1},
|
|
1267
|
+
NAME_TO_RCODE: {NXDOMAIN: 3}
|
|
1268
|
+
},
|
|
1269
|
+
A: jest.fn(data => ({type: 'A', ...data}))
|
|
1270
|
+
}))
|
|
1271
|
+
|
|
1272
|
+
jest.doMock('axios', () => ({
|
|
1273
|
+
get: jest.fn().mockResolvedValue({data: '127.0.0.1'})
|
|
1274
|
+
}))
|
|
1275
|
+
|
|
1276
|
+
mockConfig = {
|
|
1277
|
+
config: {
|
|
1278
|
+
websites: {
|
|
1279
|
+
'example.com': createMockWebsiteConfig('example.com')
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
global.Candy.setMock('core', 'Config', mockConfig)
|
|
1285
|
+
|
|
1286
|
+
jest.resetModules()
|
|
1287
|
+
DNS = require('../../server/src/DNS')
|
|
1288
|
+
})
|
|
1289
|
+
|
|
1290
|
+
afterEach(() => {
|
|
1291
|
+
cleanupGlobalMocks()
|
|
1292
|
+
jest.resetModules()
|
|
1293
|
+
jest.dontMock('native-dns')
|
|
1294
|
+
jest.dontMock('axios')
|
|
1295
|
+
})
|
|
1296
|
+
|
|
1297
|
+
it('should handle multiple IP service failures and use local network IP', async () => {
|
|
1298
|
+
const axios = require('axios')
|
|
1299
|
+
// mockLog already defined at top
|
|
1300
|
+
|
|
1301
|
+
// All services fail
|
|
1302
|
+
axios.get.mockRejectedValue(new Error('Network error'))
|
|
1303
|
+
|
|
1304
|
+
DNS.init()
|
|
1305
|
+
|
|
1306
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1307
|
+
|
|
1308
|
+
// Should fallback to local network IP (not 127.0.0.1)
|
|
1309
|
+
expect(DNS.ip).not.toBe('127.0.0.1')
|
|
1310
|
+
expect(DNS.ip).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
it('should handle second IP service success after first fails', async () => {
|
|
1314
|
+
const axios = require('axios')
|
|
1315
|
+
|
|
1316
|
+
// First service fails, second succeeds
|
|
1317
|
+
axios.get.mockRejectedValueOnce(new Error('First service failed')).mockResolvedValueOnce({data: '203.0.113.50'})
|
|
1318
|
+
|
|
1319
|
+
DNS.init()
|
|
1320
|
+
|
|
1321
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1322
|
+
|
|
1323
|
+
expect(DNS.ip).toBe('203.0.113.50')
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
it('should trim whitespace from IP response', async () => {
|
|
1327
|
+
const axios = require('axios')
|
|
1328
|
+
|
|
1329
|
+
axios.get.mockResolvedValue({data: ' 203.0.113.75 \n'})
|
|
1330
|
+
|
|
1331
|
+
DNS.init()
|
|
1332
|
+
|
|
1333
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1334
|
+
|
|
1335
|
+
expect(DNS.ip).toBe('203.0.113.75')
|
|
1336
|
+
})
|
|
1337
|
+
|
|
1338
|
+
it('should handle execSync errors in logSystemInfo', async () => {
|
|
1339
|
+
// mockLog already defined at top
|
|
1340
|
+
|
|
1341
|
+
// Mock child_process before requiring DNS
|
|
1342
|
+
jest.doMock('child_process', () => ({
|
|
1343
|
+
execSync: jest.fn(() => {
|
|
1344
|
+
throw new Error('Command failed')
|
|
1345
|
+
})
|
|
1346
|
+
}))
|
|
1347
|
+
|
|
1348
|
+
jest.resetModules()
|
|
1349
|
+
const DNSWithError = require('../../server/src/DNS')
|
|
1350
|
+
|
|
1351
|
+
DNSWithError.init()
|
|
1352
|
+
|
|
1353
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1354
|
+
|
|
1355
|
+
// Should not crash
|
|
1356
|
+
expect(mockLog).toHaveBeenCalled()
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
it('should handle platform-specific checks on non-Linux systems', async () => {
|
|
1360
|
+
// mockLog already defined at top
|
|
1361
|
+
|
|
1362
|
+
// Mock os module before requiring DNS
|
|
1363
|
+
jest.doMock('os', () => ({
|
|
1364
|
+
platform: jest.fn(() => 'darwin'),
|
|
1365
|
+
arch: jest.fn(() => 'x64'),
|
|
1366
|
+
networkInterfaces: jest.fn(() => ({
|
|
1367
|
+
en0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1368
|
+
}))
|
|
1369
|
+
}))
|
|
1370
|
+
|
|
1371
|
+
jest.resetModules()
|
|
1372
|
+
const DNSWithDarwin = require('../../server/src/DNS')
|
|
1373
|
+
|
|
1374
|
+
DNSWithDarwin.init()
|
|
1375
|
+
|
|
1376
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1377
|
+
|
|
1378
|
+
// Should skip Linux-specific checks
|
|
1379
|
+
expect(mockLog).toHaveBeenCalled()
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
it('should handle Linux platform with systemd-resolved checks', async () => {
|
|
1383
|
+
// mockLog already defined at top
|
|
1384
|
+
|
|
1385
|
+
// Mock os and child_process modules
|
|
1386
|
+
jest.doMock('os', () => ({
|
|
1387
|
+
platform: jest.fn(() => 'linux'),
|
|
1388
|
+
arch: jest.fn(() => 'x64'),
|
|
1389
|
+
networkInterfaces: jest.fn(() => ({
|
|
1390
|
+
eth0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1391
|
+
}))
|
|
1392
|
+
}))
|
|
1393
|
+
|
|
1394
|
+
jest.doMock('child_process', () => ({
|
|
1395
|
+
execSync: jest.fn(cmd => {
|
|
1396
|
+
if (cmd.includes('systemctl is-active')) return 'active'
|
|
1397
|
+
if (cmd.includes('systemd-resolve') || cmd.includes('resolvectl')) return 'DNS Server: 127.0.0.53'
|
|
1398
|
+
return ''
|
|
1399
|
+
})
|
|
1400
|
+
}))
|
|
1401
|
+
|
|
1402
|
+
jest.resetModules()
|
|
1403
|
+
const DNSWithLinux = require('../../server/src/DNS')
|
|
1404
|
+
|
|
1405
|
+
DNSWithLinux.init()
|
|
1406
|
+
|
|
1407
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1408
|
+
|
|
1409
|
+
expect(mockLog).toHaveBeenCalledWith('systemd-resolved status:', 'active')
|
|
1410
|
+
})
|
|
1411
|
+
})
|
|
1412
|
+
})
|
|
1413
|
+
|
|
1414
|
+
describe('port management and conflict resolution', () => {
|
|
1415
|
+
let DNS, mockConfig
|
|
1416
|
+
|
|
1417
|
+
beforeEach(() => {
|
|
1418
|
+
setupGlobalMocks()
|
|
1419
|
+
|
|
1420
|
+
jest.doMock('native-dns', () => ({
|
|
1421
|
+
createServer: jest.fn(() => ({
|
|
1422
|
+
on: jest.fn(),
|
|
1423
|
+
serve: jest.fn()
|
|
1424
|
+
})),
|
|
1425
|
+
createTCPServer: jest.fn(() => ({
|
|
1426
|
+
on: jest.fn(),
|
|
1427
|
+
serve: jest.fn()
|
|
1428
|
+
})),
|
|
1429
|
+
consts: {
|
|
1430
|
+
NAME_TO_QTYPE: {A: 1},
|
|
1431
|
+
NAME_TO_RCODE: {NXDOMAIN: 3}
|
|
1432
|
+
},
|
|
1433
|
+
A: jest.fn(data => ({type: 'A', ...data}))
|
|
1434
|
+
}))
|
|
1435
|
+
|
|
1436
|
+
jest.doMock('axios', () => ({
|
|
1437
|
+
get: jest.fn().mockResolvedValue({data: '127.0.0.1'})
|
|
1438
|
+
}))
|
|
1439
|
+
|
|
1440
|
+
mockConfig = {
|
|
1441
|
+
config: {
|
|
1442
|
+
websites: {
|
|
1443
|
+
'example.com': createMockWebsiteConfig('example.com')
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
global.Candy.setMock('core', 'Config', mockConfig)
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
afterEach(() => {
|
|
1452
|
+
cleanupGlobalMocks()
|
|
1453
|
+
jest.resetModules()
|
|
1454
|
+
jest.dontMock('native-dns')
|
|
1455
|
+
jest.dontMock('axios')
|
|
1456
|
+
jest.dontMock('child_process')
|
|
1457
|
+
jest.dontMock('os')
|
|
1458
|
+
jest.dontMock('fs')
|
|
1459
|
+
})
|
|
1460
|
+
|
|
1461
|
+
it('should detect port 53 is in use', async () => {
|
|
1462
|
+
// mockLog already defined at top
|
|
1463
|
+
|
|
1464
|
+
jest.doMock('child_process', () => ({
|
|
1465
|
+
execSync: jest.fn(cmd => {
|
|
1466
|
+
if (cmd.includes('lsof -i :53')) return 'systemd-resolve 1234 root 13u IPv4 12345 0t0 UDP 127.0.0.53:domain'
|
|
1467
|
+
return ''
|
|
1468
|
+
})
|
|
1469
|
+
}))
|
|
1470
|
+
|
|
1471
|
+
jest.resetModules()
|
|
1472
|
+
DNS = require('../../server/src/DNS')
|
|
1473
|
+
|
|
1474
|
+
DNS.init()
|
|
1475
|
+
|
|
1476
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
1477
|
+
|
|
1478
|
+
expect(mockLog).toHaveBeenCalledWith('Port 53 is already in use, attempting to resolve conflict...')
|
|
1479
|
+
})
|
|
1480
|
+
|
|
1481
|
+
it('should detect port is available', async () => {
|
|
1482
|
+
// mockLog already defined at top
|
|
1483
|
+
|
|
1484
|
+
jest.doMock('child_process', () => ({
|
|
1485
|
+
execSync: jest.fn(cmd => {
|
|
1486
|
+
if (cmd.includes('lsof -i :53')) return '' // Port is free
|
|
1487
|
+
return ''
|
|
1488
|
+
})
|
|
1489
|
+
}))
|
|
1490
|
+
|
|
1491
|
+
jest.resetModules()
|
|
1492
|
+
DNS = require('../../server/src/DNS')
|
|
1493
|
+
|
|
1494
|
+
DNS.init()
|
|
1495
|
+
|
|
1496
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
1497
|
+
|
|
1498
|
+
// Should attempt to start on port 53
|
|
1499
|
+
expect(mockLog).toHaveBeenCalled()
|
|
1500
|
+
})
|
|
1501
|
+
|
|
1502
|
+
it('should handle port check errors gracefully', async () => {
|
|
1503
|
+
// mockLog already defined at top
|
|
1504
|
+
|
|
1505
|
+
jest.doMock('child_process', () => ({
|
|
1506
|
+
execSync: jest.fn(() => {
|
|
1507
|
+
throw new Error('Command not found')
|
|
1508
|
+
})
|
|
1509
|
+
}))
|
|
1510
|
+
|
|
1511
|
+
jest.resetModules()
|
|
1512
|
+
DNS = require('../../server/src/DNS')
|
|
1513
|
+
|
|
1514
|
+
DNS.init()
|
|
1515
|
+
|
|
1516
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
1517
|
+
|
|
1518
|
+
expect(mockLog).toHaveBeenCalledWith('Error checking port availability:', 'Command not found')
|
|
1519
|
+
})
|
|
1520
|
+
|
|
1521
|
+
it('should detect systemd-resolved on Linux', async () => {
|
|
1522
|
+
// mockLog already defined at top
|
|
1523
|
+
|
|
1524
|
+
jest.doMock('os', () => ({
|
|
1525
|
+
platform: jest.fn(() => 'linux'),
|
|
1526
|
+
arch: jest.fn(() => 'x64'),
|
|
1527
|
+
networkInterfaces: jest.fn(() => ({
|
|
1528
|
+
eth0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1529
|
+
}))
|
|
1530
|
+
}))
|
|
1531
|
+
|
|
1532
|
+
jest.doMock('child_process', () => ({
|
|
1533
|
+
execSync: jest.fn(cmd => {
|
|
1534
|
+
if (cmd.includes('lsof -i :53')) return 'systemd-resolve 1234 root 13u IPv4 12345 0t0 UDP 127.0.0.53:domain'
|
|
1535
|
+
if (cmd.includes('systemctl is-active')) return 'active'
|
|
1536
|
+
return ''
|
|
1537
|
+
})
|
|
1538
|
+
}))
|
|
1539
|
+
|
|
1540
|
+
jest.doMock('fs', () => ({
|
|
1541
|
+
existsSync: jest.fn(() => false)
|
|
1542
|
+
}))
|
|
1543
|
+
|
|
1544
|
+
jest.resetModules()
|
|
1545
|
+
DNS = require('../../server/src/DNS')
|
|
1546
|
+
|
|
1547
|
+
DNS.init()
|
|
1548
|
+
|
|
1549
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
1550
|
+
|
|
1551
|
+
expect(mockLog).toHaveBeenCalledWith('Detected systemd-resolve using port 53, attempting resolution...')
|
|
1552
|
+
})
|
|
1553
|
+
|
|
1554
|
+
it('should skip systemd-resolved handling on non-Linux', async () => {
|
|
1555
|
+
// mockLog already defined at top
|
|
1556
|
+
|
|
1557
|
+
jest.doMock('os', () => ({
|
|
1558
|
+
platform: jest.fn(() => 'darwin'),
|
|
1559
|
+
arch: jest.fn(() => 'x64'),
|
|
1560
|
+
networkInterfaces: jest.fn(() => ({
|
|
1561
|
+
en0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1562
|
+
}))
|
|
1563
|
+
}))
|
|
1564
|
+
|
|
1565
|
+
jest.doMock('child_process', () => ({
|
|
1566
|
+
execSync: jest.fn(cmd => {
|
|
1567
|
+
if (cmd.includes('lsof -i :53')) return 'some-process 1234 user'
|
|
1568
|
+
return ''
|
|
1569
|
+
})
|
|
1570
|
+
}))
|
|
1571
|
+
|
|
1572
|
+
jest.resetModules()
|
|
1573
|
+
DNS = require('../../server/src/DNS')
|
|
1574
|
+
|
|
1575
|
+
DNS.init()
|
|
1576
|
+
|
|
1577
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
1578
|
+
|
|
1579
|
+
expect(mockLog).toHaveBeenCalledWith('Not on Linux, skipping systemd-resolve conflict resolution')
|
|
1580
|
+
})
|
|
1581
|
+
|
|
1582
|
+
it('should handle non-systemd process on port 53', async () => {
|
|
1583
|
+
// mockLog already defined at top
|
|
1584
|
+
|
|
1585
|
+
jest.doMock('os', () => ({
|
|
1586
|
+
platform: jest.fn(() => 'linux'),
|
|
1587
|
+
arch: jest.fn(() => 'x64'),
|
|
1588
|
+
networkInterfaces: jest.fn(() => ({
|
|
1589
|
+
eth0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1590
|
+
}))
|
|
1591
|
+
}))
|
|
1592
|
+
|
|
1593
|
+
jest.doMock('child_process', () => ({
|
|
1594
|
+
execSync: jest.fn(cmd => {
|
|
1595
|
+
if (cmd.includes('lsof -i :53')) return 'dnsmasq 1234 root'
|
|
1596
|
+
return ''
|
|
1597
|
+
})
|
|
1598
|
+
}))
|
|
1599
|
+
|
|
1600
|
+
jest.resetModules()
|
|
1601
|
+
DNS = require('../../server/src/DNS')
|
|
1602
|
+
|
|
1603
|
+
DNS.init()
|
|
1604
|
+
|
|
1605
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
1606
|
+
|
|
1607
|
+
expect(mockLog).toHaveBeenCalledWith('systemd-resolve not detected on port 53, conflict may be with another service')
|
|
1608
|
+
})
|
|
1609
|
+
|
|
1610
|
+
it('should handle systemd-resolved not active', async () => {
|
|
1611
|
+
// mockLog already defined at top
|
|
1612
|
+
|
|
1613
|
+
jest.doMock('os', () => ({
|
|
1614
|
+
platform: jest.fn(() => 'linux'),
|
|
1615
|
+
arch: jest.fn(() => 'x64'),
|
|
1616
|
+
networkInterfaces: jest.fn(() => ({
|
|
1617
|
+
eth0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1618
|
+
}))
|
|
1619
|
+
}))
|
|
1620
|
+
|
|
1621
|
+
jest.doMock('child_process', () => ({
|
|
1622
|
+
execSync: jest.fn(cmd => {
|
|
1623
|
+
if (cmd.includes('lsof -i :53')) return 'systemd-resolve 1234 root'
|
|
1624
|
+
if (cmd.includes('systemctl is-active')) return 'inactive'
|
|
1625
|
+
return ''
|
|
1626
|
+
})
|
|
1627
|
+
}))
|
|
1628
|
+
|
|
1629
|
+
jest.resetModules()
|
|
1630
|
+
DNS = require('../../server/src/DNS')
|
|
1631
|
+
|
|
1632
|
+
DNS.init()
|
|
1633
|
+
|
|
1634
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
1635
|
+
|
|
1636
|
+
expect(mockLog).toHaveBeenCalledWith('systemd-resolved is not active')
|
|
1637
|
+
})
|
|
1638
|
+
|
|
1639
|
+
it('should handle sudo permission errors', async () => {
|
|
1640
|
+
// mockLog already defined at top
|
|
1641
|
+
|
|
1642
|
+
jest.doMock('os', () => ({
|
|
1643
|
+
platform: jest.fn(() => 'linux'),
|
|
1644
|
+
arch: jest.fn(() => 'x64'),
|
|
1645
|
+
networkInterfaces: jest.fn(() => ({
|
|
1646
|
+
eth0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1647
|
+
}))
|
|
1648
|
+
}))
|
|
1649
|
+
|
|
1650
|
+
jest.doMock('child_process', () => ({
|
|
1651
|
+
execSync: jest.fn(cmd => {
|
|
1652
|
+
if (cmd.includes('lsof -i :53')) return 'systemd-resolve 1234 root'
|
|
1653
|
+
if (cmd.includes('systemctl is-active')) return 'active'
|
|
1654
|
+
if (cmd.includes('sudo')) throw new Error('sudo: no tty present')
|
|
1655
|
+
return ''
|
|
1656
|
+
})
|
|
1657
|
+
}))
|
|
1658
|
+
|
|
1659
|
+
jest.doMock('fs', () => ({
|
|
1660
|
+
existsSync: jest.fn(() => false)
|
|
1661
|
+
}))
|
|
1662
|
+
|
|
1663
|
+
jest.resetModules()
|
|
1664
|
+
DNS = require('../../server/src/DNS')
|
|
1665
|
+
|
|
1666
|
+
DNS.init()
|
|
1667
|
+
|
|
1668
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
1669
|
+
|
|
1670
|
+
expect(mockLog).toHaveBeenCalledWith('Could not configure systemd-resolved (no sudo access):', 'sudo: no tty present')
|
|
1671
|
+
})
|
|
1672
|
+
|
|
1673
|
+
it('should handle error handler with EADDRINUSE', async () => {
|
|
1674
|
+
const dns = require('native-dns')
|
|
1675
|
+
const mockLog = global.Candy.server('Log').init('DNS').error
|
|
1676
|
+
|
|
1677
|
+
const udpServer = {
|
|
1678
|
+
on: jest.fn(),
|
|
1679
|
+
serve: jest.fn()
|
|
1680
|
+
}
|
|
1681
|
+
const tcpServer = {
|
|
1682
|
+
on: jest.fn(),
|
|
1683
|
+
serve: jest.fn()
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
dns.createServer.mockReturnValue(udpServer)
|
|
1687
|
+
dns.createTCPServer.mockReturnValue(tcpServer)
|
|
1688
|
+
|
|
1689
|
+
jest.doMock('child_process', () => ({
|
|
1690
|
+
execSync: jest.fn(() => '')
|
|
1691
|
+
}))
|
|
1692
|
+
|
|
1693
|
+
jest.resetModules()
|
|
1694
|
+
DNS = require('../../server/src/DNS')
|
|
1695
|
+
|
|
1696
|
+
DNS.init()
|
|
1697
|
+
|
|
1698
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
1699
|
+
|
|
1700
|
+
// Get the error handler
|
|
1701
|
+
const errorHandler = udpServer.on.mock.calls.find(call => call[0] === 'error')?.[1]
|
|
1702
|
+
|
|
1703
|
+
if (errorHandler) {
|
|
1704
|
+
const error = new Error('Port in use')
|
|
1705
|
+
error.code = 'EADDRINUSE'
|
|
1706
|
+
await errorHandler(error)
|
|
1707
|
+
|
|
1708
|
+
expect(mockLog).toHaveBeenCalledWith('DNS UDP Server Error:', 'Port in use')
|
|
1709
|
+
}
|
|
1710
|
+
})
|
|
1711
|
+
|
|
1712
|
+
it('should handle error handler with EACCES', async () => {
|
|
1713
|
+
const dns = require('native-dns')
|
|
1714
|
+
const mockLog = global.Candy.server('Log').init('DNS').error
|
|
1715
|
+
|
|
1716
|
+
const udpServer = {
|
|
1717
|
+
on: jest.fn(),
|
|
1718
|
+
serve: jest.fn()
|
|
1719
|
+
}
|
|
1720
|
+
const tcpServer = {
|
|
1721
|
+
on: jest.fn(),
|
|
1722
|
+
serve: jest.fn()
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
dns.createServer.mockReturnValue(udpServer)
|
|
1726
|
+
dns.createTCPServer.mockReturnValue(tcpServer)
|
|
1727
|
+
|
|
1728
|
+
jest.doMock('child_process', () => ({
|
|
1729
|
+
execSync: jest.fn(() => '')
|
|
1730
|
+
}))
|
|
1731
|
+
|
|
1732
|
+
jest.resetModules()
|
|
1733
|
+
DNS = require('../../server/src/DNS')
|
|
1734
|
+
|
|
1735
|
+
DNS.init()
|
|
1736
|
+
|
|
1737
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
1738
|
+
|
|
1739
|
+
// Get the error handler
|
|
1740
|
+
const errorHandler = tcpServer.on.mock.calls.find(call => call[0] === 'error')?.[1]
|
|
1741
|
+
|
|
1742
|
+
if (errorHandler) {
|
|
1743
|
+
const error = new Error('Permission denied')
|
|
1744
|
+
error.code = 'EACCES'
|
|
1745
|
+
await errorHandler(error)
|
|
1746
|
+
|
|
1747
|
+
expect(mockLog).toHaveBeenCalledWith('DNS TCP Server Error:', 'Permission denied')
|
|
1748
|
+
}
|
|
1749
|
+
})
|
|
1750
|
+
})
|
|
1751
|
+
|
|
1752
|
+
describe('alternative port and system DNS configuration', () => {
|
|
1753
|
+
let DNS, mockConfig
|
|
1754
|
+
|
|
1755
|
+
beforeEach(() => {
|
|
1756
|
+
setupGlobalMocks()
|
|
1757
|
+
|
|
1758
|
+
jest.doMock('native-dns', () => ({
|
|
1759
|
+
createServer: jest.fn(() => ({
|
|
1760
|
+
on: jest.fn(),
|
|
1761
|
+
serve: jest.fn()
|
|
1762
|
+
})),
|
|
1763
|
+
createTCPServer: jest.fn(() => ({
|
|
1764
|
+
on: jest.fn(),
|
|
1765
|
+
serve: jest.fn()
|
|
1766
|
+
})),
|
|
1767
|
+
consts: {
|
|
1768
|
+
NAME_TO_QTYPE: {A: 1},
|
|
1769
|
+
NAME_TO_RCODE: {NXDOMAIN: 3}
|
|
1770
|
+
},
|
|
1771
|
+
A: jest.fn(data => ({type: 'A', ...data}))
|
|
1772
|
+
}))
|
|
1773
|
+
|
|
1774
|
+
jest.doMock('axios', () => ({
|
|
1775
|
+
get: jest.fn().mockResolvedValue({data: '127.0.0.1'})
|
|
1776
|
+
}))
|
|
1777
|
+
|
|
1778
|
+
mockConfig = {
|
|
1779
|
+
config: {
|
|
1780
|
+
websites: {
|
|
1781
|
+
'example.com': createMockWebsiteConfig('example.com')
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
global.Candy.setMock('core', 'Config', mockConfig)
|
|
1787
|
+
})
|
|
1788
|
+
|
|
1789
|
+
afterEach(() => {
|
|
1790
|
+
cleanupGlobalMocks()
|
|
1791
|
+
jest.resetModules()
|
|
1792
|
+
jest.dontMock('native-dns')
|
|
1793
|
+
jest.dontMock('axios')
|
|
1794
|
+
jest.dontMock('child_process')
|
|
1795
|
+
jest.dontMock('os')
|
|
1796
|
+
jest.dontMock('fs')
|
|
1797
|
+
})
|
|
1798
|
+
|
|
1799
|
+
it('should try alternative ports when port 53 is unavailable', async () => {
|
|
1800
|
+
// mockLog already defined at top
|
|
1801
|
+
|
|
1802
|
+
jest.doMock('os', () => ({
|
|
1803
|
+
platform: jest.fn(() => 'darwin'),
|
|
1804
|
+
arch: jest.fn(() => 'x64'),
|
|
1805
|
+
networkInterfaces: jest.fn(() => ({
|
|
1806
|
+
en0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1807
|
+
}))
|
|
1808
|
+
}))
|
|
1809
|
+
|
|
1810
|
+
jest.doMock('child_process', () => ({
|
|
1811
|
+
execSync: jest.fn(cmd => {
|
|
1812
|
+
// Port 53 is in use
|
|
1813
|
+
if (cmd.includes('lsof -i :53')) return 'some-process 1234 user'
|
|
1814
|
+
// Port 5353 is free
|
|
1815
|
+
if (cmd.includes('lsof -i :5353')) return ''
|
|
1816
|
+
return ''
|
|
1817
|
+
})
|
|
1818
|
+
}))
|
|
1819
|
+
|
|
1820
|
+
jest.resetModules()
|
|
1821
|
+
DNS = require('../../server/src/DNS')
|
|
1822
|
+
|
|
1823
|
+
DNS.init()
|
|
1824
|
+
|
|
1825
|
+
await new Promise(resolve => setTimeout(resolve, 400))
|
|
1826
|
+
|
|
1827
|
+
expect(mockLog).toHaveBeenCalledWith('Could not resolve port 53 conflict, using alternative port...')
|
|
1828
|
+
})
|
|
1829
|
+
|
|
1830
|
+
it('should handle all alternative ports in use', async () => {
|
|
1831
|
+
const mockLog = global.Candy.server('Log').init('DNS').error
|
|
1832
|
+
|
|
1833
|
+
jest.doMock('os', () => ({
|
|
1834
|
+
platform: jest.fn(() => 'darwin'),
|
|
1835
|
+
arch: jest.fn(() => 'x64'),
|
|
1836
|
+
networkInterfaces: jest.fn(() => ({
|
|
1837
|
+
en0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1838
|
+
}))
|
|
1839
|
+
}))
|
|
1840
|
+
|
|
1841
|
+
jest.doMock('child_process', () => ({
|
|
1842
|
+
execSync: jest.fn(cmd => {
|
|
1843
|
+
// All ports are in use
|
|
1844
|
+
if (cmd.includes('lsof -i :')) return 'some-process 1234 user'
|
|
1845
|
+
return ''
|
|
1846
|
+
})
|
|
1847
|
+
}))
|
|
1848
|
+
|
|
1849
|
+
jest.resetModules()
|
|
1850
|
+
DNS = require('../../server/src/DNS')
|
|
1851
|
+
|
|
1852
|
+
DNS.init()
|
|
1853
|
+
|
|
1854
|
+
await new Promise(resolve => setTimeout(resolve, 400))
|
|
1855
|
+
|
|
1856
|
+
expect(mockLog).toHaveBeenCalledWith('All alternative ports are in use')
|
|
1857
|
+
})
|
|
1858
|
+
|
|
1859
|
+
it('should handle alternative port startup errors', async () => {
|
|
1860
|
+
// mockLog already defined at top
|
|
1861
|
+
const dns = require('native-dns')
|
|
1862
|
+
|
|
1863
|
+
jest.doMock('os', () => ({
|
|
1864
|
+
platform: jest.fn(() => 'darwin'),
|
|
1865
|
+
arch: jest.fn(() => 'x64'),
|
|
1866
|
+
networkInterfaces: jest.fn(() => ({
|
|
1867
|
+
en0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1868
|
+
}))
|
|
1869
|
+
}))
|
|
1870
|
+
|
|
1871
|
+
jest.doMock('child_process', () => ({
|
|
1872
|
+
execSync: jest.fn(cmd => {
|
|
1873
|
+
if (cmd.includes('lsof -i :53')) return 'some-process 1234 user'
|
|
1874
|
+
if (cmd.includes('lsof -i :5353')) return '' // Port 5353 appears free
|
|
1875
|
+
return ''
|
|
1876
|
+
})
|
|
1877
|
+
}))
|
|
1878
|
+
|
|
1879
|
+
// Make serve throw error
|
|
1880
|
+
dns.createServer.mockReturnValue({
|
|
1881
|
+
on: jest.fn(),
|
|
1882
|
+
serve: jest.fn(() => {
|
|
1883
|
+
throw new Error('Failed to bind')
|
|
1884
|
+
})
|
|
1885
|
+
})
|
|
1886
|
+
|
|
1887
|
+
dns.createTCPServer.mockReturnValue({
|
|
1888
|
+
on: jest.fn(),
|
|
1889
|
+
serve: jest.fn(() => {
|
|
1890
|
+
throw new Error('Failed to bind')
|
|
1891
|
+
})
|
|
1892
|
+
})
|
|
1893
|
+
|
|
1894
|
+
jest.resetModules()
|
|
1895
|
+
DNS = require('../../server/src/DNS')
|
|
1896
|
+
|
|
1897
|
+
DNS.init()
|
|
1898
|
+
|
|
1899
|
+
await new Promise(resolve => setTimeout(resolve, 400))
|
|
1900
|
+
|
|
1901
|
+
expect(mockLog).toHaveBeenCalled()
|
|
1902
|
+
})
|
|
1903
|
+
|
|
1904
|
+
it('should handle serve() throwing EADDRINUSE in attemptDNSStart', async () => {
|
|
1905
|
+
// mockLog already defined at top
|
|
1906
|
+
|
|
1907
|
+
jest.doMock('native-dns', () => ({
|
|
1908
|
+
createServer: jest.fn(() => ({
|
|
1909
|
+
on: jest.fn(),
|
|
1910
|
+
serve: jest.fn(() => {
|
|
1911
|
+
const err = new Error('Address in use')
|
|
1912
|
+
err.code = 'EADDRINUSE'
|
|
1913
|
+
throw err
|
|
1914
|
+
})
|
|
1915
|
+
})),
|
|
1916
|
+
createTCPServer: jest.fn(() => ({
|
|
1917
|
+
on: jest.fn(),
|
|
1918
|
+
serve: jest.fn()
|
|
1919
|
+
})),
|
|
1920
|
+
consts: {
|
|
1921
|
+
NAME_TO_QTYPE: {A: 1},
|
|
1922
|
+
NAME_TO_RCODE: {NXDOMAIN: 3}
|
|
1923
|
+
},
|
|
1924
|
+
A: jest.fn(data => ({type: 'A', ...data}))
|
|
1925
|
+
}))
|
|
1926
|
+
|
|
1927
|
+
jest.doMock('child_process', () => ({
|
|
1928
|
+
execSync: jest.fn(() => '')
|
|
1929
|
+
}))
|
|
1930
|
+
|
|
1931
|
+
jest.resetModules()
|
|
1932
|
+
DNS = require('../../server/src/DNS')
|
|
1933
|
+
|
|
1934
|
+
// Should not throw
|
|
1935
|
+
expect(() => DNS.init()).not.toThrow()
|
|
1936
|
+
|
|
1937
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
1938
|
+
|
|
1939
|
+
// Should have logged something
|
|
1940
|
+
expect(mockLog).toHaveBeenCalled()
|
|
1941
|
+
})
|
|
1942
|
+
|
|
1943
|
+
it('should setup system DNS for internet access on port 53', async () => {
|
|
1944
|
+
// mockLog already defined at top
|
|
1945
|
+
|
|
1946
|
+
jest.doMock('child_process', () => ({
|
|
1947
|
+
execSync: jest.fn(cmd => {
|
|
1948
|
+
if (cmd.includes('lsof -i :53')) return '' // Port is free
|
|
1949
|
+
return ''
|
|
1950
|
+
})
|
|
1951
|
+
}))
|
|
1952
|
+
|
|
1953
|
+
jest.resetModules()
|
|
1954
|
+
DNS = require('../../server/src/DNS')
|
|
1955
|
+
|
|
1956
|
+
DNS.init()
|
|
1957
|
+
|
|
1958
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
1959
|
+
|
|
1960
|
+
// Should log about DNS configuration
|
|
1961
|
+
expect(mockLog).toHaveBeenCalled()
|
|
1962
|
+
})
|
|
1963
|
+
|
|
1964
|
+
it('should handle setupSystemDNSForInternet errors gracefully', async () => {
|
|
1965
|
+
// mockLog already defined at top
|
|
1966
|
+
|
|
1967
|
+
jest.doMock('child_process', () => ({
|
|
1968
|
+
execSync: jest.fn(cmd => {
|
|
1969
|
+
if (cmd.includes('lsof -i :53')) return '' // Port is free
|
|
1970
|
+
if (cmd.includes('sudo')) throw new Error('Permission denied')
|
|
1971
|
+
return ''
|
|
1972
|
+
})
|
|
1973
|
+
}))
|
|
1974
|
+
|
|
1975
|
+
jest.resetModules()
|
|
1976
|
+
DNS = require('../../server/src/DNS')
|
|
1977
|
+
|
|
1978
|
+
DNS.init()
|
|
1979
|
+
|
|
1980
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
1981
|
+
|
|
1982
|
+
expect(mockLog).toHaveBeenCalledWith('Warning: Could not configure system DNS for internet access:', 'Permission denied')
|
|
1983
|
+
})
|
|
1984
|
+
|
|
1985
|
+
it('should handle updateSystemDNSConfig errors gracefully', async () => {
|
|
1986
|
+
// mockLog already defined at top
|
|
1987
|
+
|
|
1988
|
+
jest.doMock('os', () => ({
|
|
1989
|
+
platform: jest.fn(() => 'darwin'),
|
|
1990
|
+
arch: jest.fn(() => 'x64'),
|
|
1991
|
+
networkInterfaces: jest.fn(() => ({
|
|
1992
|
+
en0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
1993
|
+
}))
|
|
1994
|
+
}))
|
|
1995
|
+
|
|
1996
|
+
jest.doMock('child_process', () => ({
|
|
1997
|
+
execSync: jest.fn(cmd => {
|
|
1998
|
+
if (cmd.includes('lsof -i :53')) return 'some-process 1234 user'
|
|
1999
|
+
if (cmd.includes('lsof -i :5353')) return '' // Port 5353 is free
|
|
2000
|
+
if (cmd.includes('sudo')) throw new Error('Permission denied')
|
|
2001
|
+
return ''
|
|
2002
|
+
})
|
|
2003
|
+
}))
|
|
2004
|
+
|
|
2005
|
+
jest.resetModules()
|
|
2006
|
+
DNS = require('../../server/src/DNS')
|
|
2007
|
+
|
|
2008
|
+
DNS.init()
|
|
2009
|
+
|
|
2010
|
+
await new Promise(resolve => setTimeout(resolve, 400))
|
|
2011
|
+
|
|
2012
|
+
expect(mockLog).toHaveBeenCalledWith('Warning: Could not update system DNS configuration:', 'Permission denied')
|
|
2013
|
+
})
|
|
2014
|
+
|
|
2015
|
+
it('should handle fs.existsSync returning true', async () => {
|
|
2016
|
+
// mockLog already defined at top
|
|
2017
|
+
|
|
2018
|
+
jest.doMock('os', () => ({
|
|
2019
|
+
platform: jest.fn(() => 'linux'),
|
|
2020
|
+
arch: jest.fn(() => 'x64'),
|
|
2021
|
+
networkInterfaces: jest.fn(() => ({
|
|
2022
|
+
eth0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
2023
|
+
}))
|
|
2024
|
+
}))
|
|
2025
|
+
|
|
2026
|
+
jest.doMock('child_process', () => ({
|
|
2027
|
+
execSync: jest.fn(cmd => {
|
|
2028
|
+
if (cmd.includes('lsof -i :53')) return 'systemd-resolve 1234 root'
|
|
2029
|
+
if (cmd.includes('systemctl is-active')) return 'active'
|
|
2030
|
+
return ''
|
|
2031
|
+
})
|
|
2032
|
+
}))
|
|
2033
|
+
|
|
2034
|
+
jest.doMock('fs', () => ({
|
|
2035
|
+
existsSync: jest.fn(() => true) // Directory already exists
|
|
2036
|
+
}))
|
|
2037
|
+
|
|
2038
|
+
jest.resetModules()
|
|
2039
|
+
DNS = require('../../server/src/DNS')
|
|
2040
|
+
|
|
2041
|
+
DNS.init()
|
|
2042
|
+
|
|
2043
|
+
await new Promise(resolve => setTimeout(resolve, 400))
|
|
2044
|
+
|
|
2045
|
+
expect(mockLog).toHaveBeenCalled()
|
|
2046
|
+
})
|
|
2047
|
+
|
|
2048
|
+
it('should handle successful systemd-resolved configuration', async () => {
|
|
2049
|
+
// mockLog already defined at top
|
|
2050
|
+
|
|
2051
|
+
jest.doMock('os', () => ({
|
|
2052
|
+
platform: jest.fn(() => 'linux'),
|
|
2053
|
+
arch: jest.fn(() => 'x64'),
|
|
2054
|
+
networkInterfaces: jest.fn(() => ({
|
|
2055
|
+
eth0: [{internal: false, family: 'IPv4', address: '192.168.1.10'}]
|
|
2056
|
+
}))
|
|
2057
|
+
}))
|
|
2058
|
+
|
|
2059
|
+
jest.doMock('child_process', () => ({
|
|
2060
|
+
execSync: jest.fn(cmd => {
|
|
2061
|
+
if (cmd.includes('lsof -i :53')) {
|
|
2062
|
+
// First call: port in use, second call after restart: port free
|
|
2063
|
+
if (cmd.match(/lsof/g)?.length === 1) return 'systemd-resolve 1234 root'
|
|
2064
|
+
return ''
|
|
2065
|
+
}
|
|
2066
|
+
if (cmd.includes('systemctl is-active')) return 'active'
|
|
2067
|
+
return ''
|
|
2068
|
+
})
|
|
2069
|
+
}))
|
|
2070
|
+
|
|
2071
|
+
jest.doMock('fs', () => ({
|
|
2072
|
+
existsSync: jest.fn(() => false)
|
|
2073
|
+
}))
|
|
2074
|
+
|
|
2075
|
+
jest.resetModules()
|
|
2076
|
+
DNS = require('../../server/src/DNS')
|
|
2077
|
+
|
|
2078
|
+
DNS.init()
|
|
2079
|
+
|
|
2080
|
+
await new Promise(resolve => setTimeout(resolve, 4000))
|
|
2081
|
+
|
|
2082
|
+
expect(mockLog).toHaveBeenCalledWith('Created systemd-resolved configuration to disable DNS stub listener')
|
|
2083
|
+
})
|
|
2084
|
+
})
|