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