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