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.
Files changed (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. 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
+ })