odac 1.0.1 → 1.2.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 (143) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +33 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/auto-pr-description.yml +3 -1
  6. package/.github/workflows/release.yml +42 -1
  7. package/.github/workflows/test-coverage.yml +6 -5
  8. package/.github/workflows/test-publish.yml +36 -0
  9. package/.husky/pre-commit +10 -0
  10. package/.husky/pre-push +13 -0
  11. package/.releaserc.js +3 -3
  12. package/CHANGELOG.md +184 -0
  13. package/README.md +53 -34
  14. package/bin/odac.js +181 -49
  15. package/client/odac.js +878 -995
  16. package/docs/backend/01-overview/03-development-server.md +39 -46
  17. package/docs/backend/02-structure/01-typical-project-layout.md +59 -25
  18. package/docs/backend/03-config/00-configuration-overview.md +15 -6
  19. package/docs/backend/03-config/01-database-connection.md +3 -3
  20. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  21. package/docs/backend/03-config/03-request-timeout.md +1 -1
  22. package/docs/backend/03-config/04-environment-variables.md +4 -4
  23. package/docs/backend/03-config/05-early-hints.md +2 -2
  24. package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
  25. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  26. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  27. package/docs/backend/04-routing/09-websocket.md +29 -0
  28. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  29. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
  30. package/docs/backend/05-controllers/03-controller-classes.md +61 -55
  31. package/docs/backend/05-forms/01-custom-forms.md +103 -95
  32. package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
  33. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  34. package/docs/backend/07-views/02-rendering-a-view.md +1 -1
  35. package/docs/backend/07-views/03-variables.md +5 -5
  36. package/docs/backend/07-views/04-request-data.md +1 -1
  37. package/docs/backend/07-views/08-backend-javascript.md +1 -1
  38. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  39. package/docs/backend/08-database/01-getting-started.md +100 -0
  40. package/docs/backend/08-database/02-basics.md +136 -0
  41. package/docs/backend/08-database/03-advanced.md +84 -0
  42. package/docs/backend/08-database/04-migrations.md +48 -0
  43. package/docs/backend/09-validation/01-the-validator-service.md +1 -0
  44. package/docs/backend/10-authentication/03-register.md +9 -2
  45. package/docs/backend/10-authentication/04-odac-register-forms.md +48 -48
  46. package/docs/backend/10-authentication/05-session-management.md +16 -2
  47. package/docs/backend/10-authentication/06-odac-login-forms.md +50 -50
  48. package/docs/backend/10-authentication/07-magic-links.md +134 -0
  49. package/docs/backend/11-mail/01-the-mail-service.md +118 -28
  50. package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
  51. package/docs/backend/13-utilities/01-odac-var.md +7 -7
  52. package/docs/backend/13-utilities/02-ipc.md +73 -0
  53. package/docs/frontend/01-overview/01-introduction.md +5 -1
  54. package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
  55. package/docs/index.json +21 -125
  56. package/eslint.config.mjs +5 -47
  57. package/jest.config.js +1 -1
  58. package/package.json +16 -7
  59. package/src/Auth.js +414 -121
  60. package/src/Config.js +12 -7
  61. package/src/Database.js +188 -0
  62. package/src/Env.js +3 -1
  63. package/src/Ipc.js +337 -0
  64. package/src/Lang.js +9 -2
  65. package/src/Mail.js +408 -37
  66. package/src/Odac.js +105 -40
  67. package/src/Request.js +71 -49
  68. package/src/Route/Cron.js +62 -18
  69. package/src/Route/Internal.js +215 -12
  70. package/src/Route/Middleware.js +7 -2
  71. package/src/Route.js +372 -109
  72. package/src/Server.js +118 -12
  73. package/src/Storage.js +169 -0
  74. package/src/Token.js +6 -4
  75. package/src/Validator.js +95 -3
  76. package/src/Var.js +22 -6
  77. package/src/View/EarlyHints.js +43 -33
  78. package/src/View/Form.js +210 -28
  79. package/src/View.js +108 -7
  80. package/src/WebSocket.js +18 -3
  81. package/template/odac.json +5 -0
  82. package/template/package.json +3 -1
  83. package/template/route/www.js +12 -10
  84. package/template/view/content/home.html +3 -3
  85. package/template/view/head/main.html +2 -2
  86. package/test/Client.test.js +168 -0
  87. package/test/Config.test.js +112 -0
  88. package/test/Lang.test.js +92 -0
  89. package/test/Odac.test.js +86 -0
  90. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  91. package/test/{framework/Route.test.js → Route.test.js} +1 -1
  92. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  93. package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
  94. package/test/scripts/check-coverage.js +4 -4
  95. package/docs/backend/08-database/01-database-connection.md +0 -99
  96. package/docs/backend/08-database/02-using-mysql.md +0 -322
  97. package/src/Mysql.js +0 -575
  98. package/template/config.json +0 -5
  99. package/test/cli/Cli.test.js +0 -36
  100. package/test/core/Candy.test.js +0 -234
  101. package/test/core/Commands.test.js +0 -538
  102. package/test/core/Config.test.js +0 -1432
  103. package/test/core/Lang.test.js +0 -250
  104. package/test/core/Process.test.js +0 -156
  105. package/test/server/Api.test.js +0 -647
  106. package/test/server/DNS.test.js +0 -2050
  107. package/test/server/DNS.test.js.bak +0 -2084
  108. package/test/server/Hub.test.js +0 -497
  109. package/test/server/Log.test.js +0 -73
  110. package/test/server/Mail.account.test_.js +0 -460
  111. package/test/server/Mail.init.test_.js +0 -411
  112. package/test/server/Mail.test_.js +0 -1340
  113. package/test/server/SSL.test_.js +0 -1491
  114. package/test/server/Server.test.js +0 -765
  115. package/test/server/Service.test_.js +0 -1127
  116. package/test/server/Subdomain.test.js +0 -440
  117. package/test/server/Web/Firewall.test.js +0 -175
  118. package/test/server/Web/Proxy.test.js +0 -397
  119. package/test/server/Web.test.js +0 -1494
  120. package/test/server/__mocks__/acme-client.js +0 -17
  121. package/test/server/__mocks__/bcrypt.js +0 -50
  122. package/test/server/__mocks__/child_process.js +0 -389
  123. package/test/server/__mocks__/crypto.js +0 -432
  124. package/test/server/__mocks__/fs.js +0 -450
  125. package/test/server/__mocks__/globalOdac.js +0 -227
  126. package/test/server/__mocks__/http.js +0 -575
  127. package/test/server/__mocks__/https.js +0 -272
  128. package/test/server/__mocks__/index.js +0 -249
  129. package/test/server/__mocks__/mail/server.js +0 -100
  130. package/test/server/__mocks__/mail/smtp.js +0 -31
  131. package/test/server/__mocks__/mailparser.js +0 -81
  132. package/test/server/__mocks__/net.js +0 -369
  133. package/test/server/__mocks__/node-forge.js +0 -328
  134. package/test/server/__mocks__/os.js +0 -320
  135. package/test/server/__mocks__/path.js +0 -291
  136. package/test/server/__mocks__/selfsigned.js +0 -8
  137. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  138. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  139. package/test/server/__mocks__/smtp-server.js +0 -106
  140. package/test/server/__mocks__/sqlite3.js +0 -394
  141. package/test/server/__mocks__/testFactories.js +0 -299
  142. package/test/server/__mocks__/testHelpers.js +0 -363
  143. package/test/server/__mocks__/tls.js +0 -229
@@ -1,1494 +0,0 @@
1
- /**
2
- * Unit tests for Web.js module
3
- * Tests web hosting, proxy functionality, and website management
4
- */
5
-
6
- // Mock all required modules before importing Web
7
- jest.mock('child_process')
8
- jest.mock('fs')
9
- jest.mock('http')
10
- jest.mock('https')
11
- jest.mock('net')
12
- jest.mock('os')
13
- jest.mock('path')
14
- jest.mock('tls')
15
-
16
- const childProcess = require('child_process')
17
- const fs = require('fs')
18
- const http = require('http')
19
- const https = require('https')
20
- const net = require('net')
21
- const os = require('os')
22
- const path = require('path')
23
- const tls = require('tls')
24
-
25
- // Import test utilities
26
- const {mockOdac} = require('./__mocks__/globalOdac')
27
- const {createMockRequest, createMockResponse} = require('./__mocks__/testFactories')
28
- const {createMockWebsiteConfig} = require('./__mocks__/testFactories')
29
-
30
- describe('Web', () => {
31
- let Web
32
- let mockConfig
33
- let mockLog
34
- let mockHttpServer
35
- let mockHttpsServer
36
-
37
- beforeEach(() => {
38
- // Reset all mocks
39
- jest.clearAllMocks()
40
-
41
- // Setup global Odac mock
42
- mockOdac.resetMocks()
43
- mockConfig = mockOdac.core('Config')
44
-
45
- // Initialize config structure
46
- mockConfig.config = {
47
- websites: {},
48
- web: {path: '/var/odac'},
49
- ssl: null
50
- }
51
-
52
- // Setup Log mock
53
- const mockLogInstance = {
54
- log: jest.fn(),
55
- error: jest.fn(),
56
- info: jest.fn(),
57
- debug: jest.fn()
58
- }
59
- mockOdac.setMock('server', 'Log', {
60
- init: jest.fn().mockReturnValue(mockLogInstance)
61
- })
62
- mockLog = mockLogInstance.log
63
-
64
- // Setup Api mock
65
- mockOdac.setMock('server', 'Api', {
66
- result: jest.fn((success, message) => ({success, message}))
67
- })
68
-
69
- // Setup DNS mock with default methods
70
- mockOdac.setMock('server', 'DNS', {
71
- record: jest.fn(),
72
- ip: '127.0.0.1'
73
- })
74
-
75
- // Setup Process mock
76
- mockOdac.setMock('core', 'Process', {
77
- stop: jest.fn()
78
- })
79
-
80
- global.Odac = mockOdac
81
- global.__ = jest.fn((key, ...args) => {
82
- // Simple mock translation function
83
- let result = key
84
- args.forEach((arg, index) => {
85
- result = result.replace(`%s${index + 1}`, arg).replace('%s', arg)
86
- })
87
- return result
88
- })
89
-
90
- // Setup mock servers
91
- mockHttpServer = {
92
- listen: jest.fn(),
93
- on: jest.fn(),
94
- close: jest.fn()
95
- }
96
-
97
- mockHttpsServer = {
98
- listen: jest.fn(),
99
- on: jest.fn(),
100
- close: jest.fn()
101
- }
102
-
103
- // Setup module mocks
104
- http.createServer.mockReturnValue(mockHttpServer)
105
- https.createServer.mockReturnValue(mockHttpsServer)
106
-
107
- // Setup http.request mock for proxy tests
108
- const mockProxyReq = {
109
- on: jest.fn(),
110
- pipe: jest.fn(),
111
- destroy: jest.fn()
112
- }
113
- http.request.mockReturnValue(mockProxyReq)
114
-
115
- // Setup file system mocks
116
- fs.existsSync.mockReturnValue(true)
117
- fs.mkdirSync.mockImplementation(() => {})
118
- fs.cpSync.mockImplementation(() => {})
119
- fs.rmSync.mockImplementation(() => {})
120
- fs.readFileSync.mockReturnValue('mock-file-content')
121
- fs.writeFile.mockImplementation((path, data, callback) => {
122
- if (callback) callback(null)
123
- })
124
-
125
- // Setup OS mocks
126
- os.homedir.mockReturnValue('/home/user')
127
- os.platform.mockReturnValue('linux')
128
-
129
- // Setup path mocks
130
- path.join.mockImplementation((...args) => args.join('/'))
131
-
132
- // Setup child process mocks
133
- const mockChild = {
134
- pid: 12345,
135
- stdout: {on: jest.fn()},
136
- stderr: {on: jest.fn()},
137
- on: jest.fn()
138
- }
139
- childProcess.spawn.mockReturnValue(mockChild)
140
- childProcess.execSync.mockImplementation(() => {})
141
-
142
- // Setup net mocks for port checking
143
- const mockNetServer = {
144
- once: jest.fn(),
145
- listen: jest.fn(),
146
- close: jest.fn()
147
- }
148
- net.createServer.mockReturnValue(mockNetServer)
149
-
150
- // Setup TLS mocks
151
- const mockSecureContext = {context: 'mock-context'}
152
- tls.createSecureContext.mockReturnValue(mockSecureContext)
153
-
154
- // Import Web after mocks are set up
155
- Web = require('../../server/src/Web')
156
- })
157
-
158
- afterEach(() => {
159
- delete global.Odac
160
- delete global.__
161
- })
162
-
163
- describe('initialization', () => {
164
- test('should initialize with default configuration', async () => {
165
- await Web.init()
166
-
167
- expect(Web.server).toBeDefined()
168
- expect(Web.server).toBeDefined()
169
- })
170
-
171
- test('should set default web path based on platform', async () => {
172
- // Test Linux/Unix platform
173
- os.platform.mockReturnValue('linux')
174
- mockConfig.config.web = undefined
175
-
176
- await Web.init()
177
-
178
- expect(mockConfig.config.web.path).toBe('/var/odac/')
179
-
180
- // Test macOS platform
181
- os.platform.mockReturnValue('darwin')
182
- mockConfig.config.web = undefined
183
-
184
- await Web.init()
185
-
186
- expect(mockConfig.config.web.path).toBe('/home/user/Odac/')
187
-
188
- // Test Windows platform
189
- os.platform.mockReturnValue('win32')
190
- mockConfig.config.web = undefined
191
-
192
- await Web.init()
193
-
194
- expect(mockConfig.config.web.path).toBe('/home/user/Odac/')
195
- })
196
-
197
- test('should create web directory if it does not exist', async () => {
198
- fs.existsSync.mockReturnValue(false)
199
- mockConfig.config.web = {path: '/custom/path'}
200
-
201
- await Web.init()
202
-
203
- expect(fs.existsSync).toHaveBeenCalledWith('/custom/path')
204
- })
205
- })
206
-
207
- describe.skip('server creation', () => {
208
- beforeEach(async () => {
209
- await Web.init()
210
- mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
211
- })
212
-
213
- test('should create HTTP server on port 80', () => {
214
- Web.server()
215
-
216
- expect(http.createServer).toHaveBeenCalledWith(expect.any(Function))
217
- expect(mockHttpServer.listen).toHaveBeenCalledWith(80)
218
- expect(mockHttpServer.on).toHaveBeenCalledWith('error', expect.any(Function))
219
- })
220
-
221
- test('should handle HTTP server errors', () => {
222
- // Create a fresh mock server for this test
223
- const freshMockHttpServer = {
224
- listen: jest.fn(),
225
- on: jest.fn(),
226
- close: jest.fn()
227
- }
228
- http.createServer.mockReturnValue(freshMockHttpServer)
229
-
230
- // Reset the Web module's server instances to force recreation
231
- Web['_Web__server_http'] = null
232
- Web['_Web__server_https'] = null
233
- Web['_Web__loaded'] = true // Ensure Web module is marked as loaded
234
-
235
- // Ensure we have websites configured (required for server creation)
236
- mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
237
-
238
- Web.server()
239
-
240
- // Verify HTTP server was created
241
- expect(http.createServer).toHaveBeenCalled()
242
-
243
- // Verify the error handler was attached
244
- expect(freshMockHttpServer.on).toHaveBeenCalledWith('error', expect.any(Function))
245
-
246
- // Get the error handler function
247
- const errorCall = freshMockHttpServer.on.mock.calls.find(call => call[0] === 'error')
248
- const errorHandler = errorCall[1]
249
-
250
- const mockError = new Error('EADDRINUSE')
251
- mockError.code = 'EADDRINUSE'
252
-
253
- expect(() => errorHandler(mockError)).not.toThrow()
254
- expect(mockLog).toHaveBeenCalledWith('HTTP server error: EADDRINUSE')
255
- expect(mockLog).toHaveBeenCalledWith('Port 80 is already in use')
256
- })
257
-
258
- test('should create HTTPS server on port 443 with SSL configuration', () => {
259
- mockConfig.config.ssl = {
260
- key: '/path/to/key.pem',
261
- cert: '/path/to/cert.pem'
262
- }
263
-
264
- Web.server()
265
-
266
- expect(https.createServer).toHaveBeenCalledWith(
267
- expect.objectContaining({
268
- SNICallback: expect.any(Function),
269
- key: 'mock-file-content',
270
- cert: 'mock-file-content'
271
- }),
272
- expect.any(Function)
273
- )
274
- expect(mockHttpsServer.listen).toHaveBeenCalledWith(443)
275
- expect(mockHttpsServer.on).toHaveBeenCalledWith('error', expect.any(Function))
276
- })
277
-
278
- test('should handle HTTPS server errors', () => {
279
- mockConfig.config.ssl = {
280
- key: '/path/to/key.pem',
281
- cert: '/path/to/cert.pem'
282
- }
283
-
284
- // Create a fresh mock server for this test
285
- const freshMockHttpsServer = {
286
- listen: jest.fn(),
287
- on: jest.fn(),
288
- close: jest.fn()
289
- }
290
- https.createServer.mockReturnValue(freshMockHttpsServer)
291
-
292
- // Reset the Web module's server instances to force recreation
293
- Web['_Web__server_http'] = null
294
- Web['_Web__server_https'] = null
295
- Web['_Web__loaded'] = true // Ensure Web module is marked as loaded
296
-
297
- // Ensure we have websites configured (required for server creation)
298
- mockConfig.config.websites = {'example.com': createMockWebsiteConfig()}
299
-
300
- Web.server()
301
-
302
- // Verify HTTPS server was created
303
- expect(https.createServer).toHaveBeenCalled()
304
-
305
- // Verify the error handler was attached
306
- expect(freshMockHttpsServer.on).toHaveBeenCalledWith('error', expect.any(Function))
307
-
308
- // Get the error handler function
309
- const errorCall = freshMockHttpsServer.on.mock.calls.find(call => call[0] === 'error')
310
- const errorHandler = errorCall[1]
311
-
312
- const mockError = new Error('EADDRINUSE')
313
- mockError.code = 'EADDRINUSE'
314
-
315
- expect(() => errorHandler(mockError)).not.toThrow()
316
- expect(mockLog).toHaveBeenCalledWith('HTTPS server error: EADDRINUSE')
317
- expect(mockLog).toHaveBeenCalledWith('Port 443 is already in use')
318
- })
319
-
320
- test('should not create HTTPS server without SSL configuration', () => {
321
- mockConfig.config.ssl = undefined
322
-
323
- Web.server()
324
-
325
- expect(https.createServer).not.toHaveBeenCalled()
326
- })
327
-
328
- test('should not create HTTPS server with missing SSL files', () => {
329
- mockConfig.config.ssl = {
330
- key: '/path/to/key.pem',
331
- cert: '/path/to/cert.pem'
332
- }
333
- fs.existsSync.mockImplementation(path => !path.includes('key.pem') && !path.includes('cert.pem'))
334
-
335
- Web.server()
336
-
337
- expect(https.createServer).not.toHaveBeenCalled()
338
- })
339
- })
340
-
341
- describe('website creation', () => {
342
- beforeEach(async () => {
343
- await Web.init()
344
- mockConfig.config.web = {path: '/var/odac'}
345
- })
346
-
347
- test('should create website with valid domain', () => {
348
- const mockProgress = jest.fn()
349
- const domain = 'example.com'
350
-
351
- const result = Web.create(domain, mockProgress)
352
-
353
- expect(result.success).toBe(true)
354
- expect(result.message).toContain('Website example.com created')
355
- expect(mockProgress).toHaveBeenCalledWith('domain', 'progress', expect.stringContaining('Setting up domain'))
356
- expect(mockProgress).toHaveBeenCalledWith('domain', 'success', expect.stringContaining('Domain example.com set'))
357
- })
358
-
359
- test('should reject invalid domain names', () => {
360
- const mockProgress = jest.fn()
361
-
362
- // Test short domain
363
- let result = Web.create('ab', mockProgress)
364
- expect(result.success).toBe(false)
365
- expect(result.message).toBe('Invalid domain.')
366
-
367
- // Test domain without dot (except localhost)
368
- result = Web.create('invalid', mockProgress)
369
- expect(result.success).toBe(false)
370
- expect(result.message).toBe('Invalid domain.')
371
- })
372
-
373
- test('should allow localhost as valid domain', () => {
374
- const mockProgress = jest.fn()
375
-
376
- const result = Web.create('localhost', mockProgress)
377
-
378
- expect(result.success).toBe(true)
379
- expect(result.message).toContain('Website localhost created')
380
- })
381
-
382
- test('should strip protocol prefixes from domain', () => {
383
- const mockProgress = jest.fn()
384
-
385
- Web.create('https://example.com', mockProgress)
386
-
387
- expect(mockConfig.config.websites['example.com']).toBeDefined()
388
- expect(mockConfig.config.websites['https://example.com']).toBeUndefined()
389
- })
390
-
391
- test('should reject existing domain', () => {
392
- const mockProgress = jest.fn()
393
- mockConfig.config.websites = {'example.com': {}}
394
-
395
- const result = Web.create('example.com', mockProgress)
396
-
397
- expect(result.success).toBe(false)
398
- expect(result.message).toBe('Website example.com already exists.')
399
- })
400
-
401
- test('should create website directory structure', () => {
402
- const mockProgress = jest.fn()
403
- const domain = 'example.com'
404
-
405
- // Mock fs.existsSync to return false for the website directory so it gets created
406
- fs.existsSync.mockImplementation(path => {
407
- if (path === '/var/odac/example.com') return false
408
- if (path.includes('node_modules')) return false
409
- return true
410
- })
411
-
412
- Web.create(domain, mockProgress)
413
-
414
- expect(fs.mkdirSync).toHaveBeenCalledWith('/var/odac/example.com', {recursive: true})
415
- expect(fs.cpSync).toHaveBeenCalledWith(expect.stringContaining('web/'), '/var/odac/example.com', {recursive: true})
416
- })
417
-
418
- test('should setup npm link for odac', () => {
419
- const mockProgress = jest.fn()
420
- const domain = 'example.com'
421
-
422
- Web.create(domain, mockProgress)
423
-
424
- expect(childProcess.execSync).toHaveBeenCalledWith('npm link odac', {
425
- cwd: '/var/odac/example.com'
426
- })
427
- })
428
-
429
- test('should remove node_modules/.bin if it exists', () => {
430
- const mockProgress = jest.fn()
431
- const domain = 'example.com'
432
- fs.existsSync.mockImplementation(path => path.includes('node_modules/.bin'))
433
-
434
- Web.create(domain, mockProgress)
435
-
436
- // Note: The actual Web.js code has a bug - missing '/' in path concatenation
437
- expect(fs.rmSync).toHaveBeenCalledWith('/var/odac/example.com/node_modules/.bin', {recursive: true})
438
- })
439
-
440
- test('should create node_modules directory if it does not exist', () => {
441
- const mockProgress = jest.fn()
442
- const domain = 'example.com'
443
- fs.existsSync.mockImplementation(path => !path.includes('node_modules'))
444
-
445
- Web.create(domain, mockProgress)
446
-
447
- expect(fs.mkdirSync).toHaveBeenCalledWith('/var/odac/example.com/node_modules')
448
- })
449
-
450
- test('should setup DNS records for non-localhost domains', () => {
451
- const mockProgress = jest.fn()
452
- const domain = 'example.com'
453
- const mockDNS = {
454
- record: jest.fn(),
455
- ip: '192.168.1.1'
456
- }
457
- mockOdac.setMock('server', 'DNS', mockDNS)
458
- mockOdac.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
459
-
460
- Web.create(domain, mockProgress)
461
-
462
- expect(mockDNS.record).toHaveBeenCalledWith(
463
- {name: 'example.com', type: 'A', value: '192.168.1.1'},
464
- {name: 'www.example.com', type: 'CNAME', value: 'example.com'},
465
- {name: 'example.com', type: 'MX', value: 'example.com'},
466
- {name: 'example.com', type: 'TXT', value: 'v=spf1 a mx ip4:192.168.1.1 ~all'},
467
- {
468
- name: '_dmarc.example.com',
469
- type: 'TXT',
470
- value: 'v=DMARC1; p=reject; rua=mailto:postmaster@example.com'
471
- }
472
- )
473
- expect(mockProgress).toHaveBeenCalledWith('dns', 'progress', expect.stringContaining('Setting up DNS records'))
474
- expect(mockProgress).toHaveBeenCalledWith('dns', 'success', expect.stringContaining('DNS records for example.com set'))
475
- })
476
-
477
- test('should not setup DNS records for localhost', () => {
478
- const mockProgress = jest.fn()
479
- const mockDNS = {record: jest.fn()}
480
- mockOdac.setMock('server', 'DNS', mockDNS)
481
- mockOdac.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
482
-
483
- Web.create('localhost', mockProgress)
484
-
485
- expect(mockDNS.record).not.toHaveBeenCalled()
486
- })
487
-
488
- test('should not setup DNS records for IP addresses', () => {
489
- const mockProgress = jest.fn()
490
- const mockDNS = {record: jest.fn()}
491
- mockOdac.setMock('server', 'DNS', mockDNS)
492
- mockOdac.setMock('server', 'Api', {result: jest.fn((success, message) => ({success, message}))})
493
-
494
- Web.create('192.168.1.1', mockProgress)
495
-
496
- expect(mockDNS.record).not.toHaveBeenCalled()
497
- })
498
- })
499
-
500
- describe.skip('request handling and proxy functionality', () => {
501
- let mockReq, mockRes
502
-
503
- beforeEach(async () => {
504
- await Web.init()
505
- mockReq = createMockRequest()
506
- mockRes = createMockResponse()
507
-
508
- // Setup a test website
509
- mockConfig.config.websites = {
510
- 'example.com': {
511
- domain: 'example.com',
512
- path: '/var/odac/example.com',
513
- pid: 12345,
514
- port: 3000,
515
- cert: {
516
- ssl: {
517
- key: '/path/to/example.key',
518
- cert: '/path/to/example.cert'
519
- }
520
- }
521
- }
522
- }
523
-
524
- // Mock watcher to indicate process is running
525
- Web['_Web__watcher'] = {12345: true}
526
- })
527
-
528
- test('should redirect HTTP requests to HTTPS', () => {
529
- mockReq.headers.host = 'example.com'
530
- mockReq.url = '/test-path'
531
-
532
- Web.request(mockReq, mockRes, false)
533
-
534
- expect(mockRes.writeHead).toHaveBeenCalled()
535
- expect(mockRes.end).toHaveBeenCalled()
536
- })
537
-
538
- test('should serve default index for requests without host header', () => {
539
- mockReq.headers = {}
540
-
541
- Web.request(mockReq, mockRes, true)
542
-
543
- expect(mockRes.write).toHaveBeenCalledWith('Odac Server')
544
- expect(mockRes.end).toHaveBeenCalled()
545
- })
546
-
547
- test('should serve default index for unknown hosts', () => {
548
- mockReq.headers.host = 'unknown.com'
549
-
550
- Web.request(mockReq, mockRes, true)
551
-
552
- expect(mockRes.write).toHaveBeenCalledWith('Odac Server')
553
- expect(mockRes.end).toHaveBeenCalled()
554
- })
555
-
556
- test('should resolve subdomain to parent domain', () => {
557
- mockReq.headers.host = 'www.example.com'
558
- mockReq.url = '/test'
559
-
560
- Web.request(mockReq, mockRes, true)
561
-
562
- expect(http.request).toHaveBeenCalledWith(
563
- expect.objectContaining({
564
- hostname: '127.0.0.1',
565
- port: 3000,
566
- path: '/test',
567
- method: 'GET'
568
- }),
569
- expect.any(Function)
570
- )
571
- })
572
-
573
- test('should proxy HTTPS requests to website process', () => {
574
- mockReq.headers.host = 'example.com'
575
- mockReq.url = '/api/test'
576
-
577
- Web.request(mockReq, mockRes, true)
578
-
579
- expect(http.request).toHaveBeenCalledWith(
580
- expect.objectContaining({
581
- hostname: '127.0.0.1',
582
- port: 3000,
583
- path: '/api/test',
584
- method: 'GET'
585
- }),
586
- expect.any(Function)
587
- )
588
- })
589
-
590
- test('should serve default index when website process is not running', () => {
591
- mockConfig.config.websites['example.com'].pid = null
592
- mockReq.headers.host = 'example.com'
593
-
594
- Web.request(mockReq, mockRes, true)
595
-
596
- expect(mockRes.write).toHaveBeenCalledWith('Odac Server')
597
- expect(mockRes.end).toHaveBeenCalled()
598
- expect(http.request).not.toHaveBeenCalled()
599
- })
600
-
601
- test('should serve default index when watcher indicates process is not running', () => {
602
- Web['_Web__watcher'] = {12345: false}
603
- mockReq.headers.host = 'example.com'
604
-
605
- Web.request(mockReq, mockRes, true)
606
-
607
- expect(mockRes.write).toHaveBeenCalledWith('Odac Server')
608
- expect(mockRes.end).toHaveBeenCalled()
609
- expect(http.request).not.toHaveBeenCalled()
610
- })
611
-
612
- test('should add custom headers to proxied requests', () => {
613
- mockReq.headers.host = 'example.com'
614
- mockReq.socket = {remoteAddress: '192.168.1.100'}
615
-
616
- Web.request(mockReq, mockRes, true)
617
-
618
- expect(http.request).toHaveBeenCalledWith(
619
- expect.objectContaining({
620
- headers: expect.objectContaining({
621
- 'x-odac-connection-remoteaddress': '192.168.1.100',
622
- 'x-odac-connection-ssl': 'true'
623
- })
624
- }),
625
- expect.any(Function)
626
- )
627
- })
628
-
629
- test('should handle exceptions in request processing', () => {
630
- mockReq.headers.host = 'example.com'
631
- http.request.mockImplementation(() => {
632
- throw new Error('Request creation failed')
633
- })
634
-
635
- Web.request(mockReq, mockRes, true)
636
-
637
- expect(mockLog).toHaveBeenCalledWith(expect.any(Error))
638
- expect(mockRes.write).toHaveBeenCalledWith('Odac Server')
639
- expect(mockRes.end).toHaveBeenCalled()
640
- })
641
-
642
- test('should handle HTTP requests with query parameters in redirection', () => {
643
- mockReq.headers.host = 'example.com'
644
- mockReq.url = '/test-path?param=value&other=123'
645
-
646
- Web.request(mockReq, mockRes, false)
647
-
648
- expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
649
- Location: 'https://example.com/test-path?param=value&other=123'
650
- })
651
- expect(mockRes.end).toHaveBeenCalled()
652
- })
653
-
654
- test('should handle HTTP requests with fragments in redirection', () => {
655
- mockReq.headers.host = 'example.com'
656
- mockReq.url = '/test-path#section'
657
-
658
- Web.request(mockReq, mockRes, false)
659
-
660
- expect(mockRes.writeHead).toHaveBeenCalledWith(301, {
661
- Location: 'https://example.com/test-path#section'
662
- })
663
- expect(mockRes.end).toHaveBeenCalled()
664
- })
665
-
666
- test('should handle multi-level subdomain resolution', () => {
667
- // Setup a multi-level subdomain scenario
668
- mockConfig.config.websites = {
669
- 'example.com': {
670
- domain: 'example.com',
671
- path: '/var/odac/example.com',
672
- pid: 12345,
673
- port: 3000
674
- }
675
- }
676
- Web['_Web__watcher'] = {12345: true}
677
-
678
- mockReq.headers.host = 'api.staging.example.com'
679
- mockReq.url = '/test'
680
-
681
- Web.request(mockReq, mockRes, true)
682
-
683
- expect(http.request).toHaveBeenCalledWith(
684
- expect.objectContaining({
685
- hostname: '127.0.0.1',
686
- port: 3000
687
- }),
688
- expect.any(Function)
689
- )
690
- })
691
-
692
- test('should handle requests with port numbers in host header', () => {
693
- mockReq.headers.host = 'example.com:8080'
694
- mockReq.url = '/test'
695
-
696
- Web.request(mockReq, mockRes, true)
697
-
698
- expect(http.request).toHaveBeenCalledWith(
699
- expect.objectContaining({
700
- hostname: '127.0.0.1',
701
- port: 3000
702
- }),
703
- expect.any(Function)
704
- )
705
- })
706
-
707
- test('should set correct SSL header for HTTPS requests', () => {
708
- mockReq.headers.host = 'example.com'
709
- mockReq.socket = {remoteAddress: '192.168.1.100'}
710
-
711
- Web.request(mockReq, mockRes, true)
712
-
713
- expect(http.request).toHaveBeenCalledWith(
714
- expect.objectContaining({
715
- headers: expect.objectContaining({
716
- 'x-odac-connection-ssl': 'true'
717
- })
718
- }),
719
- expect.any(Function)
720
- )
721
- })
722
-
723
- test('should handle missing remote address in proxy headers', () => {
724
- mockReq.headers.host = 'example.com'
725
- mockReq.socket = {}
726
-
727
- Web.request(mockReq, mockRes, true)
728
-
729
- expect(http.request).toHaveBeenCalledWith(
730
- expect.objectContaining({
731
- headers: expect.objectContaining({
732
- 'x-odac-connection-remoteaddress': '',
733
- 'x-odac-connection-ssl': 'true'
734
- })
735
- }),
736
- expect.any(Function)
737
- )
738
- })
739
- })
740
-
741
- describe.skip('process management and monitoring', () => {
742
- let mockChild
743
-
744
- beforeEach(async () => {
745
- await Web.init()
746
- mockConfig.config.web = {path: '/var/odac'}
747
-
748
- // Setup mock child process
749
- mockChild = {
750
- pid: 12345,
751
- stdout: {on: jest.fn()},
752
- stderr: {on: jest.fn()},
753
- on: jest.fn()
754
- }
755
- childProcess.spawn.mockReturnValue(mockChild)
756
-
757
- // Initialize Web module's private properties
758
- Web['_Web__active'] = {}
759
- Web['_Web__error_counts'] = {}
760
- Web['_Web__logs'] = {log: {}, err: {}}
761
- Web['_Web__ports'] = {}
762
- Web['_Web__started'] = {}
763
- Web['_Web__watcher'] = {}
764
- })
765
-
766
- test('should test port checking functionality', async () => {
767
- const mockNetServer = {
768
- once: jest.fn((event, callback) => {
769
- if (event === 'listening') {
770
- setTimeout(() => callback(), 0)
771
- }
772
- }),
773
- listen: jest.fn(),
774
- close: jest.fn()
775
- }
776
- net.createServer.mockReturnValue(mockNetServer)
777
-
778
- const result = await Web.checkPort(3000)
779
-
780
- expect(result).toBe(true)
781
- expect(mockNetServer.listen).toHaveBeenCalledWith(3000, '127.0.0.1')
782
- expect(mockNetServer.close).toHaveBeenCalled()
783
- })
784
-
785
- test('should detect port conflicts', async () => {
786
- const mockNetServer = {
787
- once: jest.fn((event, callback) => {
788
- if (event === 'error') {
789
- setTimeout(() => callback(), 0)
790
- }
791
- }),
792
- listen: jest.fn(),
793
- close: jest.fn()
794
- }
795
- net.createServer.mockReturnValue(mockNetServer)
796
-
797
- const result = await Web.checkPort(3000)
798
-
799
- expect(result).toBe(false)
800
- })
801
-
802
- test('should not start process if already active', async () => {
803
- const domain = 'example.com'
804
- mockConfig.config.websites = {
805
- [domain]: {
806
- domain,
807
- path: '/var/odac/example.com'
808
- }
809
- }
810
-
811
- // Mark domain as active
812
- Web['_Web__active'][domain] = true
813
-
814
- await Web.start(domain)
815
-
816
- expect(childProcess.spawn).not.toHaveBeenCalled()
817
- })
818
-
819
- test('should not start process if website does not exist', async () => {
820
- await Web.start('nonexistent.com')
821
-
822
- expect(childProcess.spawn).not.toHaveBeenCalled()
823
- })
824
-
825
- test('should respect error cooldown period', async () => {
826
- const domain = 'example.com'
827
- const now = Date.now()
828
- mockConfig.config.websites = {
829
- [domain]: {
830
- domain,
831
- path: '/var/odac/example.com',
832
- status: 'errored',
833
- updated: now - 500 // 500ms ago
834
- }
835
- }
836
-
837
- // Set error count to 2 (should wait 2 seconds)
838
- Web['_Web__error_counts'][domain] = 2
839
-
840
- await Web.start(domain)
841
-
842
- expect(childProcess.spawn).not.toHaveBeenCalled()
843
- })
844
-
845
- test('should not start process without index.js file', async () => {
846
- const domain = 'example.com'
847
- mockConfig.config.websites = {
848
- [domain]: {
849
- domain,
850
- path: '/var/odac/example.com'
851
- }
852
- }
853
-
854
- // Mock index.js file as missing
855
- fs.existsSync.mockImplementation(path => !path.includes('index.js'))
856
-
857
- await Web.start(domain)
858
-
859
- expect(childProcess.spawn).not.toHaveBeenCalled()
860
- expect(mockLog).toHaveBeenCalledWith("Website example.com doesn't have index.js file.")
861
- })
862
-
863
- test('should automatically restart crashed processes via check method', async () => {
864
- const domain = 'example.com'
865
- mockConfig.config.websites = {
866
- [domain]: {
867
- domain,
868
- path: '/var/odac/example.com',
869
- pid: null // No process running
870
- }
871
- }
872
-
873
- // Spy on the start method
874
- const startSpy = jest.spyOn(Web, 'start')
875
-
876
- Web.check()
877
-
878
- expect(startSpy).toHaveBeenCalledWith(domain)
879
- })
880
-
881
- test('should restart processes when watcher indicates they are not running', async () => {
882
- const domain = 'example.com'
883
- const pid = 12345
884
- mockConfig.config.websites = {
885
- [domain]: {
886
- domain,
887
- path: '/var/odac/example.com',
888
- pid
889
- }
890
- }
891
-
892
- // Mark process as not running in watcher
893
- Web['_Web__watcher'][pid] = false
894
-
895
- const mockProcess = {
896
- stop: jest.fn()
897
- }
898
- mockOdac.setMock('core', 'Process', mockProcess)
899
-
900
- const startSpy = jest.spyOn(Web, 'start')
901
-
902
- Web.check()
903
-
904
- expect(mockProcess.stop).toHaveBeenCalledWith(pid)
905
- expect(mockConfig.config.websites[domain].pid).toBeNull()
906
- expect(startSpy).toHaveBeenCalledWith(domain)
907
- })
908
-
909
- test('should write logs to files during check', async () => {
910
- const domain = 'example.com'
911
- mockConfig.config.websites = {
912
- [domain]: {
913
- domain,
914
- path: '/var/odac/example.com',
915
- pid: 12345
916
- }
917
- }
918
-
919
- // Setup logs
920
- Web['_Web__logs'].log[domain] = 'Test log content'
921
- Web['_Web__logs'].err[domain] = 'Test error content'
922
- Web['_Web__watcher'][12345] = true
923
-
924
- os.homedir.mockReturnValue('/home/user')
925
-
926
- Web.check()
927
-
928
- expect(fs.writeFile).toHaveBeenCalledWith('/home/user/.odac/logs/example.com.log', 'Test log content', expect.any(Function))
929
- expect(fs.writeFile).toHaveBeenCalledWith('/var/odac/example.com/error.log', 'Test error content', expect.any(Function))
930
- })
931
-
932
- test('should handle log file write errors gracefully', async () => {
933
- const domain = 'example.com'
934
- mockConfig.config.websites = {
935
- [domain]: {
936
- domain,
937
- path: '/var/odac/example.com',
938
- pid: 12345
939
- }
940
- }
941
-
942
- Web['_Web__logs'].log[domain] = 'Test log content'
943
- Web['_Web__watcher'][12345] = true
944
-
945
- // Mock fs.writeFile to call callback with error
946
- fs.writeFile.mockImplementation((path, data, callback) => {
947
- callback(new Error('Write failed'))
948
- })
949
-
950
- Web.check()
951
-
952
- // Should not throw, error should be logged
953
- expect(mockLog).toHaveBeenCalledWith(expect.any(Error))
954
- })
955
- })
956
-
957
- describe.skip('website deletion and resource cleanup', () => {
958
- beforeEach(async () => {
959
- await Web.init()
960
- })
961
-
962
- test('should delete website and cleanup all resources', async () => {
963
- const domain = 'example.com'
964
- const pid = 12345
965
- const port = 60000
966
-
967
- mockConfig.config.websites = {
968
- [domain]: {
969
- domain,
970
- path: '/var/odac/example.com',
971
- pid,
972
- port
973
- }
974
- }
975
-
976
- // Setup internal state
977
- Web['_Web__watcher'][pid] = true
978
- Web['_Web__ports'][port] = true
979
- Web['_Web__logs'].log[domain] = 'log content'
980
- Web['_Web__logs'].err[domain] = 'error content'
981
- Web['_Web__error_counts'][domain] = 2
982
- Web['_Web__active'][domain] = false
983
- Web['_Web__started'][domain] = Date.now()
984
-
985
- const mockProcess = {
986
- stop: jest.fn()
987
- }
988
- mockOdac.setMock('core', 'Process', mockProcess)
989
-
990
- const result = await Web.delete(domain)
991
-
992
- expect(result.success).toBe(true)
993
- expect(result.message).toContain('Website example.com deleted')
994
- expect(mockConfig.config.websites[domain]).toBeUndefined()
995
- expect(mockProcess.stop).toHaveBeenCalledWith(pid)
996
- expect(Web['_Web__watcher'][pid]).toBeUndefined()
997
- expect(Web['_Web__ports'][port]).toBeUndefined()
998
- expect(Web['_Web__logs'].log[domain]).toBeUndefined()
999
- expect(Web['_Web__logs'].err[domain]).toBeUndefined()
1000
- expect(Web['_Web__error_counts'][domain]).toBeUndefined()
1001
- expect(Web['_Web__active'][domain]).toBeUndefined()
1002
- expect(Web['_Web__started'][domain]).toBeUndefined()
1003
- })
1004
-
1005
- test('should handle deletion of non-existent website', async () => {
1006
- const result = await Web.delete('nonexistent.com')
1007
-
1008
- expect(result.success).toBe(false)
1009
- expect(result.message).toContain('Website nonexistent.com not found')
1010
- })
1011
-
1012
- test('should handle deletion of website without running process', async () => {
1013
- const domain = 'example.com'
1014
-
1015
- mockConfig.config.websites = {
1016
- [domain]: {
1017
- domain,
1018
- path: '/var/odac/example.com',
1019
- pid: null // No process running
1020
- }
1021
- }
1022
-
1023
- const result = await Web.delete(domain)
1024
-
1025
- expect(result.success).toBe(true)
1026
- expect(result.message).toContain('Website example.com deleted')
1027
- expect(mockConfig.config.websites[domain]).toBeUndefined()
1028
- })
1029
-
1030
- test('should strip protocol prefixes from domain in deletion', async () => {
1031
- const domain = 'example.com'
1032
-
1033
- mockConfig.config.websites = {
1034
- [domain]: {
1035
- domain,
1036
- path: '/var/odac/example.com'
1037
- }
1038
- }
1039
-
1040
- const result = await Web.delete('https://example.com')
1041
-
1042
- expect(result.success).toBe(true)
1043
- expect(mockConfig.config.websites[domain]).toBeUndefined()
1044
- })
1045
-
1046
- test('should stop all website processes via stopAll method', () => {
1047
- const domain1 = 'example.com'
1048
- const domain2 = 'test.com'
1049
- const pid1 = 12345
1050
- const pid2 = 67890
1051
-
1052
- mockConfig.config.websites = {
1053
- [domain1]: {domain: domain1, pid: pid1},
1054
- [domain2]: {domain: domain2, pid: pid2}
1055
- }
1056
-
1057
- const mockProcess = {
1058
- stop: jest.fn()
1059
- }
1060
- mockOdac.setMock('core', 'Process', mockProcess)
1061
-
1062
- Web.stopAll()
1063
-
1064
- expect(mockProcess.stop).toHaveBeenCalledWith(pid1)
1065
- expect(mockProcess.stop).toHaveBeenCalledWith(pid2)
1066
- expect(mockConfig.config.websites[domain1].pid).toBeNull()
1067
- expect(mockConfig.config.websites[domain2].pid).toBeNull()
1068
- })
1069
-
1070
- test('should handle stopAll with no websites', () => {
1071
- mockConfig.config.websites = {}
1072
-
1073
- const mockProcess = {
1074
- stop: jest.fn()
1075
- }
1076
- mockOdac.setMock('core', 'Process', mockProcess)
1077
-
1078
- expect(() => Web.stopAll()).not.toThrow()
1079
- expect(mockProcess.stop).not.toHaveBeenCalled()
1080
- })
1081
-
1082
- test('should handle stopAll with websites that have no running processes', () => {
1083
- const domain = 'example.com'
1084
-
1085
- mockConfig.config.websites = {
1086
- [domain]: {domain, pid: null}
1087
- }
1088
-
1089
- const mockProcess = {
1090
- stop: jest.fn()
1091
- }
1092
- mockOdac.setMock('core', 'Process', mockProcess)
1093
-
1094
- Web.stopAll()
1095
-
1096
- expect(mockProcess.stop).not.toHaveBeenCalled()
1097
- })
1098
- })
1099
-
1100
- describe.skip('SSL certificate handling and SNI', () => {
1101
- beforeEach(async () => {
1102
- await Web.init()
1103
- mockConfig.config.ssl = {
1104
- key: '/path/to/default.key',
1105
- cert: '/path/to/default.cert'
1106
- }
1107
- mockConfig.config.websites = {
1108
- 'example.com': {
1109
- domain: 'example.com',
1110
- cert: {
1111
- ssl: {
1112
- key: '/path/to/example.key',
1113
- cert: '/path/to/example.cert'
1114
- }
1115
- }
1116
- },
1117
- 'test.com': {
1118
- domain: 'test.com',
1119
- cert: false
1120
- }
1121
- }
1122
- })
1123
-
1124
- test('should use website-specific SSL certificate via SNI', () => {
1125
- Web.server()
1126
-
1127
- const httpsOptions = https.createServer.mock.calls[0][0]
1128
- const sniCallback = httpsOptions.SNICallback
1129
- const mockCallback = jest.fn()
1130
-
1131
- sniCallback('example.com', mockCallback)
1132
-
1133
- expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
1134
- expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
1135
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1136
- key: 'mock-file-content',
1137
- cert: 'mock-file-content'
1138
- })
1139
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1140
- })
1141
-
1142
- test('should fall back to default SSL certificate for websites without specific certs', () => {
1143
- Web.server()
1144
-
1145
- const httpsOptions = https.createServer.mock.calls[0][0]
1146
- const sniCallback = httpsOptions.SNICallback
1147
- const mockCallback = jest.fn()
1148
-
1149
- sniCallback('test.com', mockCallback)
1150
-
1151
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1152
- key: 'mock-file-content',
1153
- cert: 'mock-file-content'
1154
- })
1155
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1156
- })
1157
-
1158
- test('should resolve subdomain to parent domain for SSL certificate', () => {
1159
- Web.server()
1160
-
1161
- const httpsOptions = https.createServer.mock.calls[0][0]
1162
- const sniCallback = httpsOptions.SNICallback
1163
- const mockCallback = jest.fn()
1164
-
1165
- sniCallback('www.example.com', mockCallback)
1166
-
1167
- expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
1168
- expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
1169
- })
1170
-
1171
- test('should use default certificate for unknown domains', () => {
1172
- Web.server()
1173
-
1174
- const httpsOptions = https.createServer.mock.calls[0][0]
1175
- const sniCallback = httpsOptions.SNICallback
1176
- const mockCallback = jest.fn()
1177
-
1178
- sniCallback('unknown.com', mockCallback)
1179
-
1180
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1181
- key: 'mock-file-content',
1182
- cert: 'mock-file-content'
1183
- })
1184
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1185
- })
1186
-
1187
- test('should handle SSL certificate file read errors', () => {
1188
- fs.readFileSync.mockImplementation(path => {
1189
- if (path.includes('example.key')) {
1190
- throw new Error('File not found')
1191
- }
1192
- return 'mock-file-content'
1193
- })
1194
-
1195
- Web.server()
1196
-
1197
- const httpsOptions = https.createServer.mock.calls[0][0]
1198
- const sniCallback = httpsOptions.SNICallback
1199
- const mockCallback = jest.fn()
1200
-
1201
- sniCallback('example.com', mockCallback)
1202
-
1203
- expect(mockLog).toHaveBeenCalledWith('SSL certificate error for example.com: File not found')
1204
- expect(mockCallback).toHaveBeenCalledWith(expect.any(Error))
1205
- })
1206
-
1207
- test('should handle missing SSL certificate files gracefully', () => {
1208
- fs.existsSync.mockImplementation(path => !path.includes('example.key') && !path.includes('example.cert'))
1209
-
1210
- Web.server()
1211
-
1212
- const httpsOptions = https.createServer.mock.calls[0][0]
1213
- const sniCallback = httpsOptions.SNICallback
1214
- const mockCallback = jest.fn()
1215
-
1216
- sniCallback('example.com', mockCallback)
1217
-
1218
- // Should fall back to default certificate
1219
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1220
- key: 'mock-file-content',
1221
- cert: 'mock-file-content'
1222
- })
1223
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1224
- })
1225
-
1226
- test('should handle multi-level subdomain SSL certificate resolution', () => {
1227
- Web.server()
1228
-
1229
- const httpsOptions = https.createServer.mock.calls[0][0]
1230
- const sniCallback = httpsOptions.SNICallback
1231
- const mockCallback = jest.fn()
1232
-
1233
- // Test multi-level subdomain resolution (api.staging.example.com -> example.com)
1234
- sniCallback('api.staging.example.com', mockCallback)
1235
-
1236
- expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.key')
1237
- expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/example.cert')
1238
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1239
- key: 'mock-file-content',
1240
- cert: 'mock-file-content'
1241
- })
1242
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1243
- })
1244
-
1245
- test('should handle SSL certificate with missing cert property', () => {
1246
- mockConfig.config.websites['example.com'].cert = {
1247
- ssl: {
1248
- key: '/path/to/example.key'
1249
- // Missing cert property
1250
- }
1251
- }
1252
-
1253
- Web.server()
1254
-
1255
- const httpsOptions = https.createServer.mock.calls[0][0]
1256
- const sniCallback = httpsOptions.SNICallback
1257
- const mockCallback = jest.fn()
1258
-
1259
- sniCallback('example.com', mockCallback)
1260
-
1261
- // Should fall back to default certificate
1262
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1263
- key: 'mock-file-content',
1264
- cert: 'mock-file-content'
1265
- })
1266
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1267
- })
1268
-
1269
- test('should handle SSL certificate with missing key property', () => {
1270
- mockConfig.config.websites['example.com'].cert = {
1271
- ssl: {
1272
- cert: '/path/to/example.cert'
1273
- // Missing key property
1274
- }
1275
- }
1276
-
1277
- Web.server()
1278
-
1279
- const httpsOptions = https.createServer.mock.calls[0][0]
1280
- const sniCallback = httpsOptions.SNICallback
1281
- const mockCallback = jest.fn()
1282
-
1283
- sniCallback('example.com', mockCallback)
1284
-
1285
- // Should fall back to default certificate
1286
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1287
- key: 'mock-file-content',
1288
- cert: 'mock-file-content'
1289
- })
1290
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1291
- })
1292
-
1293
- test('should handle SSL certificate with missing ssl property', () => {
1294
- mockConfig.config.websites['example.com'].cert = {
1295
- // Missing ssl property
1296
- }
1297
-
1298
- Web.server()
1299
-
1300
- const httpsOptions = https.createServer.mock.calls[0][0]
1301
- const sniCallback = httpsOptions.SNICallback
1302
- const mockCallback = jest.fn()
1303
-
1304
- sniCallback('example.com', mockCallback)
1305
-
1306
- // Should fall back to default certificate
1307
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1308
- key: 'mock-file-content',
1309
- cert: 'mock-file-content'
1310
- })
1311
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1312
- })
1313
-
1314
- test('should handle hostname without dots in SNI callback', () => {
1315
- Web.server()
1316
-
1317
- const httpsOptions = https.createServer.mock.calls[0][0]
1318
- const sniCallback = httpsOptions.SNICallback
1319
- const mockCallback = jest.fn()
1320
-
1321
- sniCallback('localhost', mockCallback)
1322
-
1323
- // Should use default certificate for localhost
1324
- expect(tls.createSecureContext).toHaveBeenCalledWith({
1325
- key: 'mock-file-content',
1326
- cert: 'mock-file-content'
1327
- })
1328
- expect(mockCallback).toHaveBeenCalledWith(null, expect.any(Object))
1329
- })
1330
-
1331
- test('should handle tls.createSecureContext errors', () => {
1332
- tls.createSecureContext.mockImplementation(() => {
1333
- throw new Error('Invalid certificate format')
1334
- })
1335
-
1336
- Web.server()
1337
-
1338
- const httpsOptions = https.createServer.mock.calls[0][0]
1339
- const sniCallback = httpsOptions.SNICallback
1340
- const mockCallback = jest.fn()
1341
-
1342
- sniCallback('example.com', mockCallback)
1343
-
1344
- expect(mockLog).toHaveBeenCalledWith('SSL certificate error for example.com: Invalid certificate format')
1345
- expect(mockCallback).toHaveBeenCalledWith(expect.any(Error))
1346
- })
1347
- })
1348
-
1349
- describe('website deletion', () => {
1350
- beforeEach(async () => {
1351
- await Web.init()
1352
- mockConfig.config.websites = {
1353
- 'example.com': {
1354
- domain: 'example.com',
1355
- path: '/var/odac/example.com',
1356
- pid: 12345,
1357
- port: 3000
1358
- }
1359
- }
1360
- Web['_Web__watcher'] = {12345: true}
1361
- Web['_Web__ports'] = {3000: true}
1362
- Web['_Web__logs'] = {
1363
- log: {'example.com': 'log content'},
1364
- err: {'example.com': 'error content'}
1365
- }
1366
- Web['_Web__error_counts'] = {'example.com': 2}
1367
- Web['_Web__active'] = {'example.com': false}
1368
- Web['_Web__started'] = {'example.com': Date.now()}
1369
- })
1370
-
1371
- test('should delete website and cleanup all resources', async () => {
1372
- const mockProcess = {
1373
- stop: jest.fn()
1374
- }
1375
- mockOdac.setMock('core', 'Process', mockProcess)
1376
-
1377
- const result = await Web.delete('example.com')
1378
-
1379
- expect(result.success).toBe(true)
1380
- expect(result.message).toBe('Website example.com deleted.')
1381
- expect(mockConfig.config.websites['example.com']).toBeUndefined()
1382
- expect(mockProcess.stop).toHaveBeenCalledWith(12345)
1383
- })
1384
-
1385
- test('should strip protocol prefixes before deletion', async () => {
1386
- const result = await Web.delete('https://example.com')
1387
-
1388
- expect(result.success).toBe(true)
1389
- expect(mockConfig.config.websites['example.com']).toBeUndefined()
1390
- })
1391
-
1392
- test('should return error for non-existent website', async () => {
1393
- const result = await Web.delete('nonexistent.com')
1394
-
1395
- expect(result.success).toBe(false)
1396
- expect(result.message).toBe('Website nonexistent.com not found.')
1397
- })
1398
-
1399
- test('should handle deletion of website without running process', async () => {
1400
- mockConfig.config.websites['example.com'].pid = null
1401
-
1402
- const result = await Web.delete('example.com')
1403
-
1404
- expect(result.success).toBe(true)
1405
- expect(mockConfig.config.websites['example.com']).toBeUndefined()
1406
- })
1407
- })
1408
-
1409
- describe('utility methods', () => {
1410
- beforeEach(async () => {
1411
- await Web.init()
1412
- })
1413
-
1414
- test('should list all websites', async () => {
1415
- mockConfig.config.websites = {
1416
- 'example.com': {},
1417
- 'test.com': {},
1418
- 'demo.com': {}
1419
- }
1420
-
1421
- const result = await Web.list()
1422
-
1423
- expect(result.success).toBe(true)
1424
- expect(result.message).toContain('example.com')
1425
- expect(result.message).toContain('test.com')
1426
- expect(result.message).toContain('demo.com')
1427
- })
1428
-
1429
- test('should return error when no websites exist', async () => {
1430
- mockConfig.config.websites = {}
1431
-
1432
- const result = await Web.list()
1433
-
1434
- expect(result.success).toBe(false)
1435
- expect(result.message).toBe('No websites found.')
1436
- })
1437
-
1438
- test('should return website status', async () => {
1439
- mockConfig.config.websites = {
1440
- 'example.com': {
1441
- status: 'running',
1442
- pid: 12345
1443
- }
1444
- }
1445
-
1446
- const result = await Web.status()
1447
-
1448
- expect(result).toEqual(mockConfig.config.websites)
1449
- })
1450
-
1451
- test('should set website configuration', () => {
1452
- const websiteData = {
1453
- domain: 'example.com',
1454
- path: '/var/odac/example.com',
1455
- status: 'running'
1456
- }
1457
-
1458
- Web.set('example.com', websiteData)
1459
-
1460
- expect(mockConfig.config.websites['example.com']).toEqual(websiteData)
1461
- })
1462
-
1463
- test('should stop all websites', () => {
1464
- mockConfig.config.websites = {
1465
- 'example.com': {pid: 12345},
1466
- 'test.com': {pid: 67890},
1467
- 'demo.com': {pid: null}
1468
- }
1469
-
1470
- const mockProcess = {
1471
- stop: jest.fn()
1472
- }
1473
- mockOdac.setMock('core', 'Process', mockProcess)
1474
-
1475
- Web.stopAll()
1476
-
1477
- expect(mockProcess.stop).toHaveBeenCalledWith(12345)
1478
- expect(mockProcess.stop).toHaveBeenCalledWith(67890)
1479
- expect(mockProcess.stop).toHaveBeenCalledTimes(2)
1480
- expect(mockConfig.config.websites['example.com'].pid).toBe(null)
1481
- expect(mockConfig.config.websites['test.com'].pid).toBe(null)
1482
- })
1483
-
1484
- test('should serve default index page', () => {
1485
- const mockReq = createMockRequest()
1486
- const mockRes = createMockResponse()
1487
-
1488
- Web.index(mockReq, mockRes)
1489
-
1490
- expect(mockRes.write).toHaveBeenCalledWith('Odac Server')
1491
- expect(mockRes.end).toHaveBeenCalled()
1492
- })
1493
- })
1494
- })