odac 0.9.0 → 1.0.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 (208) hide show
  1. package/.github/workflows/auto-pr-description.yml +0 -2
  2. package/.github/workflows/codeql.yml +46 -0
  3. package/.github/workflows/release.yml +13 -6
  4. package/.github/workflows/test-coverage.yml +10 -9
  5. package/.releaserc.js +9 -6
  6. package/CHANGELOG.md +62 -150
  7. package/CODE_OF_CONDUCT.md +1 -1
  8. package/CONTRIBUTING.md +8 -8
  9. package/LICENSE +21 -661
  10. package/README.md +12 -12
  11. package/SECURITY.md +4 -4
  12. package/bin/odac.js +101 -0
  13. package/{framework/web/candy.js → client/odac.js} +310 -44
  14. package/docs/backend/01-overview/{01-whats-in-the-candy-box.md → 01-whats-in-the-odac-box.md} +4 -2
  15. package/docs/backend/01-overview/02-super-handy-helper-functions.md +29 -1
  16. package/docs/backend/01-overview/03-development-server.md +11 -11
  17. package/docs/backend/02-structure/01-typical-project-layout.md +4 -4
  18. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  19. package/docs/backend/03-config/01-database-connection.md +1 -1
  20. package/docs/backend/03-config/02-static-route-mapping-optional.md +4 -4
  21. package/docs/backend/03-config/04-environment-variables.md +20 -20
  22. package/docs/backend/03-config/05-early-hints.md +4 -4
  23. package/docs/backend/04-routing/01-basic-page-routes.md +4 -4
  24. package/docs/backend/04-routing/02-controller-less-view-routes.md +5 -5
  25. package/docs/backend/04-routing/03-api-and-data-routes.md +3 -3
  26. package/docs/backend/04-routing/04-authentication-aware-routes.md +5 -5
  27. package/docs/backend/04-routing/05-advanced-routing.md +3 -3
  28. package/docs/backend/04-routing/06-error-pages.md +17 -17
  29. package/docs/backend/04-routing/07-cron-jobs.md +13 -13
  30. package/docs/backend/04-routing/08-middleware.md +214 -0
  31. package/docs/backend/04-routing/09-websocket-auth-middleware.md +292 -0
  32. package/docs/backend/04-routing/09-websocket-examples.md +381 -0
  33. package/docs/backend/04-routing/09-websocket-quick-reference.md +211 -0
  34. package/docs/backend/04-routing/09-websocket.md +298 -0
  35. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +3 -3
  36. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +41 -0
  37. package/docs/backend/05-controllers/03-controller-classes.md +19 -19
  38. package/docs/backend/05-forms/01-custom-forms.md +114 -114
  39. package/docs/backend/05-forms/02-automatic-database-insert.md +82 -82
  40. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +26 -26
  41. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +10 -10
  42. package/docs/backend/07-views/01-the-view-directory.md +1 -1
  43. package/docs/backend/07-views/02-rendering-a-view.md +22 -22
  44. package/docs/backend/07-views/03-template-syntax.md +52 -52
  45. package/docs/backend/07-views/03-variables.md +84 -84
  46. package/docs/backend/07-views/04-request-data.md +57 -57
  47. package/docs/backend/07-views/05-conditionals.md +78 -78
  48. package/docs/backend/07-views/06-loops.md +114 -114
  49. package/docs/backend/07-views/07-translations.md +66 -66
  50. package/docs/backend/07-views/08-backend-javascript.md +103 -103
  51. package/docs/backend/07-views/09-comments.md +71 -71
  52. package/docs/backend/08-database/01-database-connection.md +8 -8
  53. package/docs/backend/08-database/02-using-mysql.md +49 -49
  54. package/docs/backend/09-validation/01-the-validator-service.md +38 -38
  55. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +15 -15
  56. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +10 -10
  57. package/docs/backend/10-authentication/03-register.md +12 -12
  58. package/docs/backend/10-authentication/{04-candy-register-forms.md → 04-odac-register-forms.md} +141 -141
  59. package/docs/backend/10-authentication/05-session-management.md +10 -10
  60. package/docs/backend/10-authentication/{06-candy-login-forms.md → 06-odac-login-forms.md} +125 -125
  61. package/docs/backend/11-mail/01-the-mail-service.md +5 -5
  62. package/docs/backend/12-streaming/01-streaming-overview.md +96 -54
  63. package/docs/backend/13-utilities/{01-candy-var.md → 01-odac-var.md} +109 -109
  64. package/docs/frontend/01-overview/01-introduction.md +30 -30
  65. package/docs/frontend/02-ajax-navigation/01-quick-start.md +45 -45
  66. package/docs/frontend/02-ajax-navigation/02-configuration.md +14 -14
  67. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +36 -36
  68. package/docs/frontend/03-forms/01-form-handling.md +32 -32
  69. package/docs/frontend/04-api-requests/01-get-post.md +33 -33
  70. package/docs/frontend/05-streaming/01-client-streaming.md +15 -15
  71. package/docs/frontend/06-websocket/00-overview.md +76 -0
  72. package/docs/frontend/06-websocket/01-websocket-client.md +139 -0
  73. package/docs/frontend/06-websocket/02-shared-websocket.md +149 -0
  74. package/docs/index.json +49 -11
  75. package/eslint.config.mjs +6 -6
  76. package/{framework/index.js → index.js} +1 -1
  77. package/package.json +14 -39
  78. package/{framework/src → src}/Auth.js +59 -59
  79. package/{framework/src → src}/Config.js +3 -3
  80. package/{framework/src → src}/Lang.js +7 -7
  81. package/{framework/src → src}/Mail.js +5 -5
  82. package/{framework/src → src}/Mysql.js +42 -42
  83. package/src/Odac.js +112 -0
  84. package/{framework/src → src}/Request.js +38 -36
  85. package/{framework/src → src}/Route/Internal.js +116 -116
  86. package/src/Route/Middleware.js +75 -0
  87. package/src/Route.js +621 -0
  88. package/src/Server.js +22 -0
  89. package/{framework/src → src}/Stream.js +11 -3
  90. package/{framework/src → src}/Validator.js +21 -21
  91. package/{framework/src → src}/Var.js +5 -5
  92. package/{framework/src → src}/View/EarlyHints.js +1 -1
  93. package/{framework/src → src}/View/Form.js +69 -69
  94. package/{framework/src → src}/View.js +78 -81
  95. package/src/WebSocket.js +403 -0
  96. package/template/config.json +5 -0
  97. package/{web → template}/controller/page/about.js +6 -6
  98. package/{web → template}/controller/page/index.js +9 -9
  99. package/{web → template}/package.json +4 -5
  100. package/{web → template}/public/assets/css/style.css +4 -4
  101. package/{web → template}/public/assets/js/app.js +6 -6
  102. package/{web → template}/route/www.js +6 -6
  103. package/{web → template}/skeleton/main.html +1 -1
  104. package/{web → template}/view/content/about.html +5 -5
  105. package/{web → template}/view/content/home.html +12 -12
  106. package/template/view/footer/main.html +11 -0
  107. package/{web → template}/view/head/main.html +1 -1
  108. package/{web → template}/view/header/main.html +2 -2
  109. package/test/core/Candy.test.js +58 -58
  110. package/test/core/Commands.test.js +7 -7
  111. package/test/core/Config.test.js +82 -85
  112. package/test/core/Lang.test.js +2 -2
  113. package/test/core/Process.test.js +6 -6
  114. package/test/framework/Route.test.js +56 -37
  115. package/test/framework/View/EarlyHints.test.js +2 -2
  116. package/test/framework/WebSocket.test.js +100 -0
  117. package/test/framework/middleware.test.js +85 -0
  118. package/test/server/Api.test.js +31 -31
  119. package/test/server/DNS.test.js +11 -11
  120. package/test/server/Hub.test.js +497 -0
  121. package/test/server/Mail.account.test_.js +3 -3
  122. package/test/server/Mail.init.test_.js +10 -10
  123. package/test/server/Mail.test_.js +20 -20
  124. package/test/server/SSL.test_.js +54 -54
  125. package/test/server/Server.test.js +39 -39
  126. package/test/server/Service.test_.js +7 -7
  127. package/test/server/Subdomain.test.js +7 -7
  128. package/test/server/Web/Firewall.test.js +87 -87
  129. package/test/server/Web/Proxy.test.js +397 -0
  130. package/test/server/{Web.test_.js → Web.test.js} +137 -205
  131. package/test/server/__mocks__/fs.js +2 -2
  132. package/test/server/__mocks__/{globalCandy.js → globalOdac.js} +5 -5
  133. package/test/server/__mocks__/index.js +6 -6
  134. package/test/server/__mocks__/testFactories.js +1 -1
  135. package/test/server/__mocks__/testHelpers.js +7 -7
  136. package/.husky/pre-commit +0 -2
  137. package/.kiro/steering/code-style.md +0 -56
  138. package/.kiro/steering/product.md +0 -20
  139. package/.kiro/steering/structure.md +0 -77
  140. package/.kiro/steering/tech.md +0 -87
  141. package/AGENTS.md +0 -84
  142. package/bin/candy +0 -10
  143. package/bin/candypack +0 -10
  144. package/cli/index.js +0 -3
  145. package/cli/src/Cli.js +0 -348
  146. package/cli/src/Connector.js +0 -93
  147. package/cli/src/Monitor.js +0 -416
  148. package/core/Candy.js +0 -87
  149. package/core/Commands.js +0 -239
  150. package/core/Config.js +0 -1094
  151. package/core/Lang.js +0 -52
  152. package/core/Log.js +0 -43
  153. package/core/Process.js +0 -26
  154. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +0 -20
  155. package/docs/server/01-installation/01-quick-install.md +0 -19
  156. package/docs/server/01-installation/02-manual-installation-via-npm.md +0 -9
  157. package/docs/server/02-get-started/01-core-concepts.md +0 -7
  158. package/docs/server/02-get-started/02-basic-commands.md +0 -57
  159. package/docs/server/02-get-started/03-cli-reference.md +0 -276
  160. package/docs/server/02-get-started/04-cli-quick-reference.md +0 -102
  161. package/docs/server/03-service/01-start-a-new-service.md +0 -57
  162. package/docs/server/03-service/02-delete-a-service.md +0 -48
  163. package/docs/server/04-web/01-create-a-website.md +0 -36
  164. package/docs/server/04-web/02-list-websites.md +0 -9
  165. package/docs/server/04-web/03-delete-a-website.md +0 -29
  166. package/docs/server/05-subdomain/01-create-a-subdomain.md +0 -32
  167. package/docs/server/05-subdomain/02-list-subdomains.md +0 -33
  168. package/docs/server/05-subdomain/03-delete-a-subdomain.md +0 -41
  169. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +0 -34
  170. package/docs/server/07-mail/01-create-a-mail-account.md +0 -23
  171. package/docs/server/07-mail/02-delete-a-mail-account.md +0 -20
  172. package/docs/server/07-mail/03-list-mail-accounts.md +0 -20
  173. package/docs/server/07-mail/04-change-account-password.md +0 -23
  174. package/framework/src/Candy.js +0 -81
  175. package/framework/src/Route.js +0 -455
  176. package/framework/src/Server.js +0 -15
  177. package/locale/de-DE.json +0 -80
  178. package/locale/en-US.json +0 -79
  179. package/locale/es-ES.json +0 -80
  180. package/locale/fr-FR.json +0 -80
  181. package/locale/pt-BR.json +0 -80
  182. package/locale/ru-RU.json +0 -80
  183. package/locale/tr-TR.json +0 -85
  184. package/locale/zh-CN.json +0 -80
  185. package/server/index.js +0 -5
  186. package/server/src/Api.js +0 -88
  187. package/server/src/DNS.js +0 -940
  188. package/server/src/Hub.js +0 -535
  189. package/server/src/Mail.js +0 -571
  190. package/server/src/SSL.js +0 -180
  191. package/server/src/Server.js +0 -27
  192. package/server/src/Service.js +0 -248
  193. package/server/src/Subdomain.js +0 -64
  194. package/server/src/Web/Firewall.js +0 -170
  195. package/server/src/Web/Proxy.js +0 -134
  196. package/server/src/Web.js +0 -451
  197. package/server/src/mail/imap.js +0 -1091
  198. package/server/src/mail/server.js +0 -32
  199. package/server/src/mail/smtp.js +0 -786
  200. package/test/server/Client.test.js +0 -338
  201. package/test/server/__mocks__/http-proxy.js +0 -105
  202. package/watchdog/index.js +0 -3
  203. package/watchdog/src/Watchdog.js +0 -156
  204. package/web/config.json +0 -5
  205. package/web/view/footer/main.html +0 -11
  206. /package/{framework/src → src}/Env.js +0 -0
  207. /package/{framework/src → src}/Route/Cron.js +0 -0
  208. /package/{framework/src → src}/Token.js +0 -0
@@ -22,8 +22,8 @@ describe('DNS Module', () => {
22
22
  setupGlobalMocks()
23
23
 
24
24
  // Set up the Log mock before requiring DNS
25
- const {mockCandy} = require('./__mocks__/globalCandy')
26
- mockCandy.setMock('core', 'Log', {
25
+ const {mockOdac} = require('./__mocks__/globalOdac')
26
+ mockOdac.setMock('core', 'Log', {
27
27
  init: jest.fn().mockReturnValue({
28
28
  log: mockLog,
29
29
  error: mockError
@@ -75,7 +75,7 @@ describe('DNS Module', () => {
75
75
  }
76
76
  }
77
77
 
78
- global.Candy.setMock('core', 'Config', mockConfig)
78
+ global.Odac.setMock('core', 'Config', mockConfig)
79
79
 
80
80
  // Clear module cache and require DNS
81
81
  jest.resetModules()
@@ -105,7 +105,7 @@ describe('DNS Module', () => {
105
105
  DNS.init()
106
106
 
107
107
  expect(axios.get).toHaveBeenCalledWith('https://curlmyip.org/', {
108
- headers: {'User-Agent': 'CandyPack-DNS/1.0'},
108
+ headers: {'User-Agent': 'Odac-DNS/1.0'},
109
109
  timeout: 5000
110
110
  })
111
111
  })
@@ -1282,7 +1282,7 @@ describe('DNS Module', () => {
1282
1282
  }
1283
1283
  }
1284
1284
 
1285
- global.Candy.setMock('core', 'Config', mockConfig)
1285
+ global.Odac.setMock('core', 'Config', mockConfig)
1286
1286
 
1287
1287
  jest.resetModules()
1288
1288
  DNS = require('../../server/src/DNS')
@@ -1416,8 +1416,8 @@ describe('port management and conflict resolution', () => {
1416
1416
  setupGlobalMocks()
1417
1417
 
1418
1418
  // Set up the Log mock before requiring DNS
1419
- const {mockCandy} = require('./__mocks__/globalCandy')
1420
- mockCandy.setMock('core', 'Log', {
1419
+ const {mockOdac} = require('./__mocks__/globalOdac')
1420
+ mockOdac.setMock('core', 'Log', {
1421
1421
  init: jest.fn().mockReturnValue({
1422
1422
  log: mockLog,
1423
1423
  error: mockError
@@ -1452,7 +1452,7 @@ describe('port management and conflict resolution', () => {
1452
1452
  }
1453
1453
  }
1454
1454
 
1455
- global.Candy.setMock('core', 'Config', mockConfig)
1455
+ global.Odac.setMock('core', 'Config', mockConfig)
1456
1456
  })
1457
1457
 
1458
1458
  afterEach(() => {
@@ -1742,8 +1742,8 @@ describe('alternative port and system DNS configuration', () => {
1742
1742
  setupGlobalMocks()
1743
1743
 
1744
1744
  // Set up the Log mock before requiring DNS
1745
- const {mockCandy} = require('./__mocks__/globalCandy')
1746
- mockCandy.setMock('core', 'Log', {
1745
+ const {mockOdac} = require('./__mocks__/globalOdac')
1746
+ mockOdac.setMock('core', 'Log', {
1747
1747
  init: jest.fn().mockReturnValue({
1748
1748
  log: mockLog,
1749
1749
  error: mockError
@@ -1778,7 +1778,7 @@ describe('alternative port and system DNS configuration', () => {
1778
1778
  }
1779
1779
  }
1780
1780
 
1781
- global.Candy.setMock('core', 'Config', mockConfig)
1781
+ global.Odac.setMock('core', 'Config', mockConfig)
1782
1782
  })
1783
1783
 
1784
1784
  afterEach(() => {
@@ -0,0 +1,497 @@
1
+ const mockLog = jest.fn()
2
+ const mockError = jest.fn()
3
+
4
+ const {mockOdac} = require('./__mocks__/globalOdac')
5
+
6
+ mockOdac.setMock('core', 'Log', {
7
+ init: jest.fn().mockReturnValue({
8
+ log: mockLog,
9
+ error: mockError
10
+ })
11
+ })
12
+
13
+ global.Odac = mockOdac
14
+
15
+ jest.mock('axios')
16
+ const axios = require('axios')
17
+
18
+ jest.mock('ws')
19
+
20
+ jest.mock('os')
21
+ const os = require('os')
22
+
23
+ jest.mock('fs')
24
+ const fs = require('fs')
25
+
26
+ jest.useFakeTimers()
27
+
28
+ describe('Hub', () => {
29
+ let Hub
30
+
31
+ beforeEach(() => {
32
+ jest.clearAllMocks()
33
+ jest.clearAllTimers()
34
+
35
+ mockOdac.setMock('core', 'Config', {
36
+ config: {
37
+ hub: null,
38
+ server: {started: Date.now()},
39
+ websites: {},
40
+ services: [],
41
+ mail: {accounts: {}}
42
+ }
43
+ })
44
+
45
+ mockOdac.setMock('server', 'Api', {
46
+ result: jest.fn((success, message) => ({success, message}))
47
+ })
48
+
49
+ os.hostname.mockReturnValue('test-host')
50
+ os.platform.mockReturnValue('linux')
51
+ os.arch.mockReturnValue('x64')
52
+ os.totalmem.mockReturnValue(8589934592)
53
+ os.freemem.mockReturnValue(4294967296)
54
+ os.cpus.mockReturnValue([
55
+ {times: {user: 1000, nice: 0, sys: 500, idle: 8500, irq: 0}},
56
+ {times: {user: 1000, nice: 0, sys: 500, idle: 8500, irq: 0}}
57
+ ])
58
+
59
+ jest.isolateModules(() => {
60
+ Hub = require('../../server/src/Hub')
61
+ })
62
+ })
63
+
64
+ afterEach(() => {
65
+ if (Hub.websocket) {
66
+ Hub.websocket = null
67
+ }
68
+ Hub.checkCounter = 0
69
+ })
70
+
71
+ describe('initialization', () => {
72
+ it('should initialize with default values', () => {
73
+ expect(Hub.websocket).toBeNull()
74
+ expect(Hub.websocketReconnectAttempts).toBe(0)
75
+ expect(Hub.maxReconnectAttempts).toBe(5)
76
+ expect(Hub.checkCounter).toBe(0)
77
+ })
78
+ })
79
+
80
+ describe('check counter', () => {
81
+ it('should increment counter on each check', () => {
82
+ expect(Hub.checkCounter).toBe(0)
83
+ Hub.check()
84
+ expect(Hub.checkCounter).toBe(1)
85
+ Hub.check()
86
+ expect(Hub.checkCounter).toBe(2)
87
+ })
88
+
89
+ it('should reset counter after reaching 60', () => {
90
+ Hub.checkCounter = 59
91
+ Hub.check()
92
+ expect(Hub.checkCounter).toBe(0)
93
+ })
94
+
95
+ it('should skip API call when counter is not 0', async () => {
96
+ mockOdac.setMock('core', 'Config', {
97
+ config: {
98
+ hub: {token: 'test-token', secret: 'test-secret'},
99
+ server: {started: Date.now()}
100
+ }
101
+ })
102
+
103
+ Hub.checkCounter = 1
104
+ await Hub.check()
105
+ expect(axios.post).not.toHaveBeenCalled()
106
+ })
107
+
108
+ it('should skip API call when websocket is connected', async () => {
109
+ mockOdac.setMock('core', 'Config', {
110
+ config: {
111
+ hub: {token: 'test-token', secret: 'test-secret'},
112
+ server: {started: Date.now()}
113
+ }
114
+ })
115
+
116
+ Hub.websocket = {readyState: 1}
117
+ Hub.checkCounter = 0
118
+ await Hub.check()
119
+ expect(axios.post).not.toHaveBeenCalled()
120
+ })
121
+ })
122
+
123
+ describe('check', () => {
124
+ beforeEach(() => {
125
+ Hub.checkCounter = 0
126
+ })
127
+
128
+ it('should return early if no hub config', async () => {
129
+ mockOdac.setMock('core', 'Config', {
130
+ config: {hub: null}
131
+ })
132
+
133
+ await Hub.check()
134
+ expect(axios.post).not.toHaveBeenCalled()
135
+ })
136
+
137
+ it('should return early if no token', async () => {
138
+ mockOdac.setMock('core', 'Config', {
139
+ config: {hub: {}}
140
+ })
141
+
142
+ await Hub.check()
143
+ expect(axios.post).not.toHaveBeenCalled()
144
+ })
145
+
146
+ it('should send status to hub when counter is 0', async () => {
147
+ jest.useRealTimers()
148
+
149
+ mockOdac.setMock('core', 'Config', {
150
+ config: {
151
+ hub: {token: 'test-token', secret: 'test-secret'},
152
+ server: {started: Date.now()}
153
+ }
154
+ })
155
+
156
+ axios.post.mockResolvedValue({
157
+ data: {
158
+ result: {success: true},
159
+ data: {authenticated: true}
160
+ }
161
+ })
162
+
163
+ Hub.checkCounter = 59
164
+ Hub.check()
165
+
166
+ await new Promise(resolve => setImmediate(resolve))
167
+ expect(axios.post).toHaveBeenCalled()
168
+
169
+ jest.useFakeTimers()
170
+ })
171
+
172
+ it('should handle authentication failure', async () => {
173
+ jest.useRealTimers()
174
+
175
+ mockOdac.setMock('core', 'Config', {
176
+ config: {
177
+ hub: {token: 'invalid-token', secret: 'test-secret'},
178
+ server: {started: Date.now()}
179
+ }
180
+ })
181
+
182
+ axios.post.mockResolvedValue({
183
+ data: {
184
+ result: {success: true},
185
+ data: {authenticated: false, reason: 'token_invalid'}
186
+ }
187
+ })
188
+
189
+ Hub.checkCounter = 59
190
+ Hub.check()
191
+
192
+ await new Promise(resolve => setImmediate(resolve))
193
+ expect(mockLog).toHaveBeenCalledWith('Server not authenticated: %s', 'token_invalid')
194
+
195
+ jest.useFakeTimers()
196
+ })
197
+
198
+ it('should clear config on invalid token', async () => {
199
+ jest.useRealTimers()
200
+
201
+ const config = {
202
+ hub: {token: 'invalid-token', secret: 'test-secret'},
203
+ server: {started: Date.now()}
204
+ }
205
+ mockOdac.setMock('core', 'Config', {config})
206
+
207
+ axios.post.mockResolvedValue({
208
+ data: {
209
+ result: {success: true},
210
+ data: {authenticated: false, reason: 'token_invalid'}
211
+ }
212
+ })
213
+
214
+ Hub.checkCounter = 59
215
+ Hub.check()
216
+
217
+ await new Promise(resolve => setImmediate(resolve))
218
+ expect(config.hub).toBeUndefined()
219
+
220
+ jest.useFakeTimers()
221
+ })
222
+
223
+ it('should handle check errors gracefully', async () => {
224
+ jest.useRealTimers()
225
+
226
+ mockOdac.setMock('core', 'Config', {
227
+ config: {
228
+ hub: {token: 'test-token', secret: 'test-secret'},
229
+ server: {started: Date.now()}
230
+ }
231
+ })
232
+
233
+ axios.post.mockRejectedValue(new Error('Network error'))
234
+
235
+ Hub.checkCounter = 59
236
+ Hub.check()
237
+
238
+ await new Promise(resolve => setImmediate(resolve))
239
+ expect(mockLog).toHaveBeenCalledWith('Failed to report status: %s', 'Network error')
240
+
241
+ jest.useFakeTimers()
242
+ })
243
+ })
244
+
245
+ describe('authentication', () => {
246
+ it('should authenticate with valid code', async () => {
247
+ const mockResponse = {
248
+ data: {
249
+ result: {success: true},
250
+ data: {
251
+ token: 'new-token',
252
+ secret: 'new-secret'
253
+ }
254
+ }
255
+ }
256
+
257
+ const mockApiResult = {success: true, message: 'Authentication successful'}
258
+ mockOdac.setMock('server', 'Api', {
259
+ result: jest.fn(() => mockApiResult)
260
+ })
261
+
262
+ axios.post.mockResolvedValue(mockResponse)
263
+
264
+ const result = await Hub.auth('valid-code')
265
+
266
+ expect(axios.post).toHaveBeenCalled()
267
+ expect(result).toEqual(mockApiResult)
268
+ expect(mockOdac.core('Config').config.hub).toEqual({
269
+ token: 'new-token',
270
+ secret: 'new-secret'
271
+ })
272
+ })
273
+
274
+ it('should handle authentication failure', async () => {
275
+ const mockApiResult = {success: false, message: 'Authentication failed'}
276
+ mockOdac.setMock('server', 'Api', {
277
+ result: jest.fn(() => mockApiResult)
278
+ })
279
+
280
+ axios.post.mockRejectedValue(new Error('Invalid code'))
281
+
282
+ const result = await Hub.auth('invalid-code')
283
+
284
+ expect(result).toEqual(mockApiResult)
285
+ expect(mockLog).toHaveBeenCalledWith('Authentication failed: %s', 'Invalid code')
286
+ })
287
+
288
+ it('should include distro info on Linux', async () => {
289
+ os.platform.mockReturnValue('linux')
290
+ fs.readFileSync.mockReturnValue('NAME="Ubuntu"\nVERSION_ID="20.04"\nID=ubuntu')
291
+
292
+ axios.post.mockResolvedValue({
293
+ data: {
294
+ result: {success: true},
295
+ data: {token: 'token', secret: 'secret'}
296
+ }
297
+ })
298
+
299
+ await Hub.auth('code')
300
+
301
+ const callArgs = axios.post.mock.calls[0][1]
302
+ expect(callArgs.distro).toBeDefined()
303
+ expect(callArgs.distro.name).toBe('Ubuntu')
304
+ })
305
+ })
306
+
307
+ describe('system status', () => {
308
+ it('should get system status', () => {
309
+ const status = Hub.getSystemStatus()
310
+
311
+ expect(status).toHaveProperty('cpu')
312
+ expect(status).toHaveProperty('memory')
313
+ expect(status).toHaveProperty('disk')
314
+ expect(status).toHaveProperty('network')
315
+ expect(status).toHaveProperty('services')
316
+ expect(status).toHaveProperty('uptime')
317
+ expect(status.hostname).toBe('test-host')
318
+ expect(status.platform).toBe('linux')
319
+ expect(status.arch).toBe('x64')
320
+ })
321
+
322
+ it('should get services info', () => {
323
+ mockOdac.setMock('core', 'Config', {
324
+ config: {
325
+ websites: {
326
+ 'example.com': {},
327
+ 'test.com': {}
328
+ },
329
+ services: ['web', 'mail'],
330
+ mail: {
331
+ accounts: {
332
+ 'user@example.com': {},
333
+ 'admin@test.com': {}
334
+ }
335
+ }
336
+ }
337
+ })
338
+
339
+ const services = Hub.getServicesInfo()
340
+
341
+ expect(services.websites).toBe(2)
342
+ expect(services.services).toBe(2)
343
+ expect(services.mail).toBe(2)
344
+ })
345
+
346
+ it('should handle missing services config', () => {
347
+ mockOdac.setMock('core', 'Config', {config: {}})
348
+
349
+ const services = Hub.getServicesInfo()
350
+
351
+ expect(services.websites).toBe(0)
352
+ expect(services.services).toBe(0)
353
+ expect(services.mail).toBe(0)
354
+ })
355
+ })
356
+
357
+ describe('memory usage', () => {
358
+ it('should get memory usage on Linux', () => {
359
+ os.platform.mockReturnValue('linux')
360
+ os.totalmem.mockReturnValue(8589934592)
361
+ os.freemem.mockReturnValue(4294967296)
362
+
363
+ const memory = Hub.getMemoryUsage()
364
+
365
+ expect(memory.total).toBe(8589934592)
366
+ expect(memory.used).toBe(4294967296)
367
+ })
368
+ })
369
+
370
+ describe('CPU usage', () => {
371
+ it('should return 0 on first call', () => {
372
+ const usage = Hub.getCpuUsage()
373
+ expect(usage).toBe(0)
374
+ })
375
+
376
+ it('should calculate CPU usage on subsequent calls', () => {
377
+ Hub.getCpuUsage()
378
+
379
+ os.cpus.mockReturnValue([
380
+ {times: {user: 2000, nice: 0, sys: 1000, idle: 7000, irq: 0}},
381
+ {times: {user: 2000, nice: 0, sys: 1000, idle: 7000, irq: 0}}
382
+ ])
383
+
384
+ const usage = Hub.getCpuUsage()
385
+ expect(usage).toBeGreaterThanOrEqual(0)
386
+ expect(usage).toBeLessThanOrEqual(100)
387
+ })
388
+ })
389
+
390
+ describe('request signing', () => {
391
+ it('should sign request with secret', () => {
392
+ mockOdac.setMock('core', 'Config', {
393
+ config: {
394
+ hub: {token: 'token', secret: 'test-secret'}
395
+ }
396
+ })
397
+
398
+ const data = {test: 'data'}
399
+ const signature = Hub.signRequest(data)
400
+
401
+ expect(signature).toBeTruthy()
402
+ expect(typeof signature).toBe('string')
403
+ })
404
+
405
+ it('should return null without secret', () => {
406
+ mockOdac.setMock('core', 'Config', {
407
+ config: {hub: {token: 'token'}}
408
+ })
409
+
410
+ const signature = Hub.signRequest({test: 'data'})
411
+ expect(signature).toBeNull()
412
+ })
413
+ })
414
+
415
+ describe('API calls', () => {
416
+ it('should make successful API call', async () => {
417
+ axios.post.mockResolvedValue({
418
+ data: {
419
+ result: {success: true},
420
+ data: {response: 'data'}
421
+ }
422
+ })
423
+
424
+ const result = await Hub.call('test-action', {param: 'value'})
425
+
426
+ expect(result).toEqual({response: 'data'})
427
+ expect(axios.post).toHaveBeenCalledWith('https://hub.odac.run/test-action', {param: 'value'}, expect.any(Object))
428
+ })
429
+
430
+ it('should include authorization header when token exists', async () => {
431
+ mockOdac.setMock('core', 'Config', {
432
+ config: {
433
+ hub: {token: 'test-token', secret: 'test-secret'}
434
+ }
435
+ })
436
+
437
+ axios.post.mockResolvedValue({
438
+ data: {
439
+ result: {success: true},
440
+ data: {}
441
+ }
442
+ })
443
+
444
+ await Hub.call('test', {})
445
+
446
+ const callArgs = axios.post.mock.calls[0][2]
447
+ expect(callArgs.headers.Authorization).toBe('Bearer test-token')
448
+ })
449
+
450
+ it('should handle API errors', async () => {
451
+ axios.post.mockResolvedValue({
452
+ data: {
453
+ result: {success: false, message: 'API error'}
454
+ }
455
+ })
456
+
457
+ await expect(Hub.call('test', {})).rejects.toBe('API error')
458
+ })
459
+
460
+ it('should handle network errors', async () => {
461
+ axios.post.mockRejectedValue({
462
+ response: {status: 500, data: 'Server error'}
463
+ })
464
+
465
+ await expect(Hub.call('test', {})).rejects.toBe('Server error')
466
+ })
467
+ })
468
+
469
+ describe('Linux distro detection', () => {
470
+ it('should return null on non-Linux platforms', () => {
471
+ os.platform.mockReturnValue('darwin')
472
+ const distro = Hub.getLinuxDistro()
473
+ expect(distro).toBeNull()
474
+ })
475
+
476
+ it('should parse os-release file', () => {
477
+ os.platform.mockReturnValue('linux')
478
+ fs.readFileSync.mockReturnValue('NAME="Ubuntu"\nVERSION_ID="20.04"\nID=ubuntu')
479
+
480
+ const distro = Hub.getLinuxDistro()
481
+
482
+ expect(distro.name).toBe('Ubuntu')
483
+ expect(distro.version).toBe('20.04')
484
+ expect(distro.id).toBe('ubuntu')
485
+ })
486
+
487
+ it('should handle missing os-release file', () => {
488
+ os.platform.mockReturnValue('linux')
489
+ fs.readFileSync.mockImplementation(() => {
490
+ throw new Error('File not found')
491
+ })
492
+
493
+ const distro = Hub.getLinuxDistro()
494
+ expect(distro).toBeNull()
495
+ })
496
+ })
497
+ })
@@ -78,9 +78,9 @@ describe('Mail Module - Account Management Operations', () => {
78
78
  result: jest.fn((success, data) => ({success, data}))
79
79
  }
80
80
 
81
- // Setup global Candy mocks
82
- global.Candy.setMock('core', 'Config', mockConfig)
83
- global.Candy.setMock('server', 'Api', mockApi)
81
+ // Setup global Odac mocks
82
+ global.Odac.setMock('core', 'Config', mockConfig)
83
+ global.Odac.setMock('server', 'Api', mockApi)
84
84
 
85
85
  // Setup bcrypt mock
86
86
  bcrypt.hash.mockImplementation((password, rounds, callback) => {
@@ -61,9 +61,9 @@ describe('Mail Module - Server Initialization and Database Setup', () => {
61
61
  }
62
62
  }
63
63
 
64
- // Setup global Candy mocks
65
- global.Candy.setMock('core', 'Config', mockConfig)
66
- global.Candy.setMock('server', 'Log', {
64
+ // Setup global Odac mocks
65
+ global.Odac.setMock('core', 'Config', mockConfig)
66
+ global.Odac.setMock('server', 'Log', {
67
67
  init: jest.fn().mockReturnValue({
68
68
  log: jest.fn(),
69
69
  error: jest.fn()
@@ -111,7 +111,7 @@ describe('Mail Module - Server Initialization and Database Setup', () => {
111
111
  expect.objectContaining({
112
112
  logger: true,
113
113
  secure: false,
114
- banner: 'CandyPack',
114
+ banner: 'Odac',
115
115
  size: 1024 * 1024 * 10,
116
116
  authOptional: true
117
117
  })
@@ -153,7 +153,7 @@ describe('Mail Module - Server Initialization and Database Setup', () => {
153
153
  expect.objectContaining({
154
154
  logger: true,
155
155
  secure: false,
156
- banner: 'CandyPack'
156
+ banner: 'Odac'
157
157
  })
158
158
  )
159
159
 
@@ -179,7 +179,7 @@ describe('Mail Module - Server Initialization and Database Setup', () => {
179
179
  Mail.init()
180
180
 
181
181
  // Assert
182
- expect(sqlite3.verbose().Database).toHaveBeenCalledWith('/home/user/.candypack/db/mail', expect.any(Function))
182
+ expect(sqlite3.verbose().Database).toHaveBeenCalledWith('/home/user/.odac/db/mail', expect.any(Function))
183
183
  })
184
184
 
185
185
  test('should create mail database tables on initialization', () => {
@@ -210,7 +210,7 @@ describe('Mail Module - Server Initialization and Database Setup', () => {
210
210
  Mail.init()
211
211
 
212
212
  // Assert
213
- expect(fs.mkdirSync).toHaveBeenCalledWith('/home/user/.candypack/db', {recursive: true})
213
+ expect(fs.mkdirSync).toHaveBeenCalledWith('/home/user/.odac/db', {recursive: true})
214
214
  })
215
215
 
216
216
  test('should handle database connection errors', () => {
@@ -225,7 +225,7 @@ describe('Mail Module - Server Initialization and Database Setup', () => {
225
225
  })
226
226
  })
227
227
 
228
- global.Candy.setMock('server', 'Log', {
228
+ global.Odac.setMock('server', 'Log', {
229
229
  init: jest.fn().mockReturnValue({
230
230
  log: jest.fn(),
231
231
  error: mockError
@@ -365,7 +365,7 @@ describe('Mail Module - Server Initialization and Database Setup', () => {
365
365
  test('should handle SMTP server errors', () => {
366
366
  // Arrange
367
367
  const mockLog = jest.fn()
368
- global.Candy.setMock('server', 'Log', {
368
+ global.Odac.setMock('server', 'Log', {
369
369
  init: jest.fn().mockReturnValue({
370
370
  log: mockLog,
371
371
  error: jest.fn()
@@ -388,7 +388,7 @@ describe('Mail Module - Server Initialization and Database Setup', () => {
388
388
  test('should handle secure SMTP server errors', () => {
389
389
  // Arrange
390
390
  const mockError = jest.fn()
391
- global.Candy.setMock('server', 'Log', {
391
+ global.Odac.setMock('server', 'Log', {
392
392
  init: jest.fn().mockReturnValue({
393
393
  log: jest.fn(),
394
394
  error: mockError