odac 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- package/web/view/header/main.html +14 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive unit tests for the Api.js module
|
|
3
|
+
* Tests TCP server functionality, authentication, and command routing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {setupGlobalMocks, cleanupGlobalMocks} = require('./__mocks__/testHelpers')
|
|
7
|
+
|
|
8
|
+
// Create mock log function first
|
|
9
|
+
const mockLog = jest.fn()
|
|
10
|
+
const mockError = jest.fn()
|
|
11
|
+
|
|
12
|
+
describe('Api', () => {
|
|
13
|
+
let Api
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
setupGlobalMocks()
|
|
17
|
+
|
|
18
|
+
// Set up the Log mock before requiring Api
|
|
19
|
+
const {mockCandy} = require('./__mocks__/globalCandy')
|
|
20
|
+
mockCandy.setMock('core', 'Log', {
|
|
21
|
+
init: jest.fn().mockReturnValue({
|
|
22
|
+
log: mockLog,
|
|
23
|
+
error: mockError
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Mock the net module at the module level
|
|
28
|
+
jest.doMock('net', () => ({
|
|
29
|
+
createServer: jest.fn(() => ({
|
|
30
|
+
on: jest.fn(),
|
|
31
|
+
listen: jest.fn()
|
|
32
|
+
}))
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
// Mock the crypto module at the module level
|
|
36
|
+
jest.doMock('crypto', () => ({
|
|
37
|
+
randomBytes: jest.fn(() => Buffer.from('mock-auth-token-32-bytes-long-test'))
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
// Clear module cache and require Api
|
|
41
|
+
jest.resetModules()
|
|
42
|
+
Api = require('../../server/src/Api')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
cleanupGlobalMocks()
|
|
47
|
+
jest.resetModules()
|
|
48
|
+
jest.dontMock('net')
|
|
49
|
+
jest.dontMock('crypto')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('initialization', () => {
|
|
53
|
+
it('should initialize api config if not exists', () => {
|
|
54
|
+
// Clear the api config
|
|
55
|
+
global.Candy.core('Config').config.api = undefined
|
|
56
|
+
|
|
57
|
+
Api.init()
|
|
58
|
+
|
|
59
|
+
expect(global.Candy.core('Config').config.api).toBeDefined()
|
|
60
|
+
expect(global.Candy.core('Config').config.api.auth).toBeDefined()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should create TCP server and set up handlers', () => {
|
|
64
|
+
const net = require('net')
|
|
65
|
+
const mockServer = {
|
|
66
|
+
on: jest.fn(),
|
|
67
|
+
listen: jest.fn()
|
|
68
|
+
}
|
|
69
|
+
net.createServer.mockReturnValue(mockServer)
|
|
70
|
+
|
|
71
|
+
Api.init()
|
|
72
|
+
|
|
73
|
+
expect(net.createServer).toHaveBeenCalled()
|
|
74
|
+
expect(mockServer.listen).toHaveBeenCalledWith(1453)
|
|
75
|
+
expect(mockServer.on).toHaveBeenCalledWith('connection', expect.any(Function))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should generate auth token', () => {
|
|
79
|
+
const crypto = require('crypto')
|
|
80
|
+
|
|
81
|
+
Api.init()
|
|
82
|
+
|
|
83
|
+
expect(crypto.randomBytes).toHaveBeenCalledWith(32)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('connection handling', () => {
|
|
88
|
+
let mockServer
|
|
89
|
+
let connectionHandler
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
const net = require('net')
|
|
93
|
+
mockServer = {
|
|
94
|
+
on: jest.fn(),
|
|
95
|
+
listen: jest.fn()
|
|
96
|
+
}
|
|
97
|
+
net.createServer.mockReturnValue(mockServer)
|
|
98
|
+
|
|
99
|
+
Api.init()
|
|
100
|
+
|
|
101
|
+
// Get the connection handler
|
|
102
|
+
const connectionCall = mockServer.on.mock.calls.find(call => call[0] === 'connection')
|
|
103
|
+
connectionHandler = connectionCall ? connectionCall[1] : null
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should accept connections from localhost IPv4', () => {
|
|
107
|
+
if (!connectionHandler) {
|
|
108
|
+
fail('Connection handler not found')
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const mockSocket = {
|
|
113
|
+
remoteAddress: '127.0.0.1',
|
|
114
|
+
on: jest.fn(),
|
|
115
|
+
destroy: jest.fn()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
connectionHandler(mockSocket)
|
|
119
|
+
|
|
120
|
+
expect(mockSocket.destroy).not.toHaveBeenCalled()
|
|
121
|
+
expect(mockSocket.on).toHaveBeenCalledWith('data', expect.any(Function))
|
|
122
|
+
expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should accept connections from localhost IPv6', () => {
|
|
126
|
+
if (!connectionHandler) {
|
|
127
|
+
fail('Connection handler not found')
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const mockSocket = {
|
|
132
|
+
remoteAddress: '::ffff:127.0.0.1',
|
|
133
|
+
on: jest.fn(),
|
|
134
|
+
destroy: jest.fn()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
connectionHandler(mockSocket)
|
|
138
|
+
|
|
139
|
+
expect(mockSocket.destroy).not.toHaveBeenCalled()
|
|
140
|
+
expect(mockSocket.on).toHaveBeenCalledWith('data', expect.any(Function))
|
|
141
|
+
expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function))
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should reject connections from non-localhost addresses', () => {
|
|
145
|
+
if (!connectionHandler) {
|
|
146
|
+
fail('Connection handler not found')
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const mockSocket = {
|
|
151
|
+
remoteAddress: '192.168.1.100',
|
|
152
|
+
on: jest.fn(),
|
|
153
|
+
destroy: jest.fn()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
connectionHandler(mockSocket)
|
|
157
|
+
|
|
158
|
+
expect(mockSocket.destroy).toHaveBeenCalled()
|
|
159
|
+
expect(mockSocket.on).not.toHaveBeenCalled()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should reject connections from external IPv6 addresses', () => {
|
|
163
|
+
if (!connectionHandler) {
|
|
164
|
+
fail('Connection handler not found')
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const mockSocket = {
|
|
169
|
+
remoteAddress: '::ffff:192.168.1.100',
|
|
170
|
+
on: jest.fn(),
|
|
171
|
+
destroy: jest.fn()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
connectionHandler(mockSocket)
|
|
175
|
+
|
|
176
|
+
expect(mockSocket.destroy).toHaveBeenCalled()
|
|
177
|
+
expect(mockSocket.on).not.toHaveBeenCalled()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should clean up connection on close', () => {
|
|
181
|
+
if (!connectionHandler) {
|
|
182
|
+
fail('Connection handler not found')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const mockSocket = {
|
|
187
|
+
remoteAddress: '127.0.0.1',
|
|
188
|
+
on: jest.fn(),
|
|
189
|
+
destroy: jest.fn()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
connectionHandler(mockSocket)
|
|
193
|
+
|
|
194
|
+
// Get the close handler
|
|
195
|
+
const closeCall = mockSocket.on.mock.calls.find(call => call[0] === 'close')
|
|
196
|
+
expect(closeCall).toBeDefined()
|
|
197
|
+
|
|
198
|
+
const closeHandler = closeCall[1]
|
|
199
|
+
|
|
200
|
+
// Simulate connection close
|
|
201
|
+
closeHandler()
|
|
202
|
+
|
|
203
|
+
// Connection should be cleaned up (tested indirectly through no errors)
|
|
204
|
+
expect(closeHandler).toBeDefined()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('data processing and authentication', () => {
|
|
209
|
+
let mockServer
|
|
210
|
+
let connectionHandler
|
|
211
|
+
let dataHandler
|
|
212
|
+
let mockSocket
|
|
213
|
+
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
const net = require('net')
|
|
216
|
+
mockServer = {
|
|
217
|
+
on: jest.fn(),
|
|
218
|
+
listen: jest.fn()
|
|
219
|
+
}
|
|
220
|
+
net.createServer.mockReturnValue(mockServer)
|
|
221
|
+
|
|
222
|
+
Api.init()
|
|
223
|
+
|
|
224
|
+
// Get the connection handler
|
|
225
|
+
const connectionCall = mockServer.on.mock.calls.find(call => call[0] === 'connection')
|
|
226
|
+
connectionHandler = connectionCall ? connectionCall[1] : null
|
|
227
|
+
|
|
228
|
+
if (!connectionHandler) {
|
|
229
|
+
fail('Connection handler not found')
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Set up a localhost connection
|
|
234
|
+
mockSocket = {
|
|
235
|
+
remoteAddress: '127.0.0.1',
|
|
236
|
+
on: jest.fn(),
|
|
237
|
+
write: jest.fn(),
|
|
238
|
+
destroy: jest.fn()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
connectionHandler(mockSocket)
|
|
242
|
+
|
|
243
|
+
// Get the data handler
|
|
244
|
+
const dataCall = mockSocket.on.mock.calls.find(call => call[0] === 'data')
|
|
245
|
+
dataHandler = dataCall ? dataCall[1] : null
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should return invalid_json error for malformed JSON', async () => {
|
|
249
|
+
if (!dataHandler) {
|
|
250
|
+
fail('Data handler not found')
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const invalidJson = Buffer.from('invalid json data')
|
|
255
|
+
|
|
256
|
+
await dataHandler(invalidJson)
|
|
257
|
+
|
|
258
|
+
expect(mockSocket.write).toHaveBeenCalledWith(JSON.stringify(Api.result(false, 'invalid_json')))
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should return unauthorized error for missing auth token', async () => {
|
|
262
|
+
if (!dataHandler) {
|
|
263
|
+
fail('Data handler not found')
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const payload = JSON.stringify({
|
|
268
|
+
action: 'mail.list',
|
|
269
|
+
data: []
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
await dataHandler(Buffer.from(payload))
|
|
273
|
+
|
|
274
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":false'))
|
|
275
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"message":"unauthorized"'))
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should return unauthorized error for invalid auth token', async () => {
|
|
279
|
+
if (!dataHandler) {
|
|
280
|
+
fail('Data handler not found')
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const payload = JSON.stringify({
|
|
285
|
+
auth: 'invalid-token',
|
|
286
|
+
action: 'mail.list',
|
|
287
|
+
data: []
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
await dataHandler(Buffer.from(payload))
|
|
291
|
+
|
|
292
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":false'))
|
|
293
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"message":"unauthorized"'))
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should return unknown_action error for invalid action', async () => {
|
|
297
|
+
if (!dataHandler) {
|
|
298
|
+
fail('Data handler not found')
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const payload = JSON.stringify({
|
|
303
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
304
|
+
action: 'invalid.action',
|
|
305
|
+
data: []
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
await dataHandler(Buffer.from(payload))
|
|
309
|
+
|
|
310
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":false'))
|
|
311
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"message":"unknown_action"'))
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should return unknown_action error for missing action', async () => {
|
|
315
|
+
if (!dataHandler) {
|
|
316
|
+
fail('Data handler not found')
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const payload = JSON.stringify({
|
|
321
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
322
|
+
data: []
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
await dataHandler(Buffer.from(payload))
|
|
326
|
+
|
|
327
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":false'))
|
|
328
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"message":"unknown_action"'))
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('should execute valid mail.create command', async () => {
|
|
332
|
+
if (!dataHandler) {
|
|
333
|
+
fail('Data handler not found')
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Mock the Mail service
|
|
338
|
+
const mockMailService = global.Candy.server('Mail')
|
|
339
|
+
mockMailService.create.mockResolvedValue(Api.result(true, 'Account created'))
|
|
340
|
+
|
|
341
|
+
const payload = JSON.stringify({
|
|
342
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
343
|
+
action: 'mail.create',
|
|
344
|
+
data: ['test@example.com', 'password123']
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
await dataHandler(Buffer.from(payload))
|
|
348
|
+
|
|
349
|
+
expect(mockMailService.create).toHaveBeenCalledWith('test@example.com', 'password123', expect.any(Function))
|
|
350
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":true'))
|
|
351
|
+
expect(mockSocket.destroy).toHaveBeenCalled()
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should execute valid service.start command', async () => {
|
|
355
|
+
if (!dataHandler) {
|
|
356
|
+
fail('Data handler not found')
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const mockServiceService = global.Candy.server('Service')
|
|
361
|
+
mockServiceService.start.mockResolvedValue(Api.result(true, 'Service started'))
|
|
362
|
+
|
|
363
|
+
const payload = JSON.stringify({
|
|
364
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
365
|
+
action: 'service.start',
|
|
366
|
+
data: ['my-service.js']
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
await dataHandler(Buffer.from(payload))
|
|
370
|
+
|
|
371
|
+
expect(mockServiceService.start).toHaveBeenCalledWith('my-service.js', expect.any(Function))
|
|
372
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":true'))
|
|
373
|
+
expect(mockSocket.destroy).toHaveBeenCalled()
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should execute valid server.stop command', async () => {
|
|
377
|
+
if (!dataHandler) {
|
|
378
|
+
fail('Data handler not found')
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const mockServerService = global.Candy.server('Server')
|
|
383
|
+
mockServerService.stop.mockResolvedValue(Api.result(true, 'Server stopped'))
|
|
384
|
+
|
|
385
|
+
const payload = JSON.stringify({
|
|
386
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
387
|
+
action: 'server.stop',
|
|
388
|
+
data: []
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
await dataHandler(Buffer.from(payload))
|
|
392
|
+
|
|
393
|
+
expect(mockServerService.stop).toHaveBeenCalledWith()
|
|
394
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":true'))
|
|
395
|
+
expect(mockSocket.destroy).toHaveBeenCalled()
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('should handle command execution errors', async () => {
|
|
399
|
+
if (!dataHandler) {
|
|
400
|
+
fail('Data handler not found')
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const mockMailService = global.Candy.server('Mail')
|
|
405
|
+
mockMailService.create.mockRejectedValue(new Error('Database connection failed'))
|
|
406
|
+
|
|
407
|
+
const payload = JSON.stringify({
|
|
408
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
409
|
+
action: 'mail.create',
|
|
410
|
+
data: ['test@example.com', 'password123']
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
await dataHandler(Buffer.from(payload))
|
|
414
|
+
|
|
415
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":false'))
|
|
416
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"message":"Database connection failed"'))
|
|
417
|
+
expect(mockSocket.destroy).toHaveBeenCalled()
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('should handle commands with no data parameter', async () => {
|
|
421
|
+
if (!dataHandler) {
|
|
422
|
+
fail('Data handler not found')
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const mockMailService = global.Candy.server('Mail')
|
|
427
|
+
mockMailService.list.mockResolvedValue(Api.result(true, []))
|
|
428
|
+
|
|
429
|
+
const payload = JSON.stringify({
|
|
430
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
431
|
+
action: 'mail.list'
|
|
432
|
+
// No data parameter
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
await dataHandler(Buffer.from(payload))
|
|
436
|
+
|
|
437
|
+
expect(mockMailService.list).toHaveBeenCalledWith(expect.any(Function))
|
|
438
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":true'))
|
|
439
|
+
expect(mockSocket.destroy).toHaveBeenCalled()
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('should execute all mail commands', async () => {
|
|
443
|
+
if (!dataHandler) {
|
|
444
|
+
fail('Data handler not found')
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const mockMailService = global.Candy.server('Mail')
|
|
449
|
+
mockMailService.delete.mockResolvedValue(Api.result(true, 'Deleted'))
|
|
450
|
+
mockMailService.password.mockResolvedValue(Api.result(true, 'Password changed'))
|
|
451
|
+
mockMailService.send.mockResolvedValue(Api.result(true, 'Email sent'))
|
|
452
|
+
|
|
453
|
+
// Test mail.delete
|
|
454
|
+
let payload = JSON.stringify({
|
|
455
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
456
|
+
action: 'mail.delete',
|
|
457
|
+
data: ['test@example.com']
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
await dataHandler(Buffer.from(payload))
|
|
461
|
+
expect(mockMailService.delete).toHaveBeenCalledWith('test@example.com', expect.any(Function))
|
|
462
|
+
|
|
463
|
+
// Test mail.password
|
|
464
|
+
payload = JSON.stringify({
|
|
465
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
466
|
+
action: 'mail.password',
|
|
467
|
+
data: ['test@example.com', 'newpassword']
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
await dataHandler(Buffer.from(payload))
|
|
471
|
+
expect(mockMailService.password).toHaveBeenCalledWith('test@example.com', 'newpassword', expect.any(Function))
|
|
472
|
+
|
|
473
|
+
// Test mail.send
|
|
474
|
+
payload = JSON.stringify({
|
|
475
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
476
|
+
action: 'mail.send',
|
|
477
|
+
data: ['test@example.com', 'Subject', 'Body']
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
await dataHandler(Buffer.from(payload))
|
|
481
|
+
expect(mockMailService.send).toHaveBeenCalledWith('test@example.com', 'Subject', 'Body', expect.any(Function))
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('should execute all subdomain commands', async () => {
|
|
485
|
+
if (!dataHandler) {
|
|
486
|
+
fail('Data handler not found')
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const mockSubdomainService = global.Candy.server('Subdomain')
|
|
491
|
+
mockSubdomainService.create.mockResolvedValue(Api.result(true, 'Created'))
|
|
492
|
+
mockSubdomainService.delete.mockResolvedValue(Api.result(true, 'Deleted'))
|
|
493
|
+
mockSubdomainService.list.mockResolvedValue(Api.result(true, ['www', 'api']))
|
|
494
|
+
|
|
495
|
+
// Test subdomain.create
|
|
496
|
+
let payload = JSON.stringify({
|
|
497
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
498
|
+
action: 'subdomain.create',
|
|
499
|
+
data: ['api.example.com']
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
await dataHandler(Buffer.from(payload))
|
|
503
|
+
expect(mockSubdomainService.create).toHaveBeenCalledWith('api.example.com', expect.any(Function))
|
|
504
|
+
|
|
505
|
+
// Test subdomain.delete
|
|
506
|
+
payload = JSON.stringify({
|
|
507
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
508
|
+
action: 'subdomain.delete',
|
|
509
|
+
data: ['api.example.com']
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
await dataHandler(Buffer.from(payload))
|
|
513
|
+
expect(mockSubdomainService.delete).toHaveBeenCalledWith('api.example.com', expect.any(Function))
|
|
514
|
+
|
|
515
|
+
// Test subdomain.list
|
|
516
|
+
payload = JSON.stringify({
|
|
517
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
518
|
+
action: 'subdomain.list',
|
|
519
|
+
data: ['example.com']
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
await dataHandler(Buffer.from(payload))
|
|
523
|
+
expect(mockSubdomainService.list).toHaveBeenCalledWith('example.com', expect.any(Function))
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('should execute all web commands', async () => {
|
|
527
|
+
if (!dataHandler) {
|
|
528
|
+
fail('Data handler not found')
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const mockWebService = global.Candy.server('Web')
|
|
533
|
+
mockWebService.create.mockResolvedValue(Api.result(true, 'Created'))
|
|
534
|
+
mockWebService.delete.mockResolvedValue(Api.result(true, 'Deleted'))
|
|
535
|
+
mockWebService.list.mockResolvedValue(Api.result(true, ['example.com']))
|
|
536
|
+
|
|
537
|
+
// Test web.create
|
|
538
|
+
let payload = JSON.stringify({
|
|
539
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
540
|
+
action: 'web.create',
|
|
541
|
+
data: ['example.com']
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
await dataHandler(Buffer.from(payload))
|
|
545
|
+
expect(mockWebService.create).toHaveBeenCalledWith('example.com', expect.any(Function))
|
|
546
|
+
|
|
547
|
+
// Test web.delete
|
|
548
|
+
payload = JSON.stringify({
|
|
549
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
550
|
+
action: 'web.delete',
|
|
551
|
+
data: ['example.com']
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
await dataHandler(Buffer.from(payload))
|
|
555
|
+
expect(mockWebService.delete).toHaveBeenCalledWith('example.com', expect.any(Function))
|
|
556
|
+
|
|
557
|
+
// Test web.list
|
|
558
|
+
payload = JSON.stringify({
|
|
559
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
560
|
+
action: 'web.list',
|
|
561
|
+
data: []
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
await dataHandler(Buffer.from(payload))
|
|
565
|
+
expect(mockWebService.list).toHaveBeenCalledWith(expect.any(Function))
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
it('should execute ssl.renew command', async () => {
|
|
569
|
+
if (!dataHandler) {
|
|
570
|
+
fail('Data handler not found')
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const mockSSLService = global.Candy.server('SSL')
|
|
575
|
+
mockSSLService.renew.mockResolvedValue(Api.result(true, 'SSL renewed'))
|
|
576
|
+
|
|
577
|
+
const payload = JSON.stringify({
|
|
578
|
+
auth: global.Candy.core('Config').config.api.auth,
|
|
579
|
+
action: 'ssl.renew',
|
|
580
|
+
data: ['example.com']
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
await dataHandler(Buffer.from(payload))
|
|
584
|
+
|
|
585
|
+
expect(mockSSLService.renew).toHaveBeenCalledWith('example.com', expect.any(Function))
|
|
586
|
+
expect(mockSocket.write).toHaveBeenCalledWith(expect.stringContaining('"result":true'))
|
|
587
|
+
expect(mockSocket.destroy).toHaveBeenCalled()
|
|
588
|
+
})
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
describe('utility methods', () => {
|
|
592
|
+
it('should format result correctly', () => {
|
|
593
|
+
const successResult = Api.result(true, 'Operation successful')
|
|
594
|
+
expect(successResult).toEqual({
|
|
595
|
+
result: true,
|
|
596
|
+
message: 'Operation successful'
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
const errorResult = Api.result(false, 'Operation failed')
|
|
600
|
+
expect(errorResult).toEqual({
|
|
601
|
+
result: false,
|
|
602
|
+
message: 'Operation failed'
|
|
603
|
+
})
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('should handle send to non-existent connection gracefully', () => {
|
|
607
|
+
Api.init()
|
|
608
|
+
|
|
609
|
+
// Try to send to non-existent connection
|
|
610
|
+
const result = Api.send('non-existent-id', 'test-process', 'running', 'Test message')
|
|
611
|
+
|
|
612
|
+
// Should not throw and should return undefined
|
|
613
|
+
expect(result).toBeUndefined()
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('should send messages to active connections', () => {
|
|
617
|
+
const net = require('net')
|
|
618
|
+
const mockServer = {
|
|
619
|
+
on: jest.fn(),
|
|
620
|
+
listen: jest.fn()
|
|
621
|
+
}
|
|
622
|
+
net.createServer.mockReturnValue(mockServer)
|
|
623
|
+
|
|
624
|
+
Api.init()
|
|
625
|
+
|
|
626
|
+
// Set up a connection
|
|
627
|
+
const connectionCall = mockServer.on.mock.calls.find(call => call[0] === 'connection')
|
|
628
|
+
const connectionHandler = connectionCall[1]
|
|
629
|
+
|
|
630
|
+
const mockSocket = {
|
|
631
|
+
remoteAddress: '127.0.0.1',
|
|
632
|
+
on: jest.fn(),
|
|
633
|
+
write: jest.fn(),
|
|
634
|
+
destroy: jest.fn()
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
connectionHandler(mockSocket)
|
|
638
|
+
|
|
639
|
+
// The send method exists and can be called
|
|
640
|
+
expect(Api.send).toBeDefined()
|
|
641
|
+
expect(typeof Api.send).toBe('function')
|
|
642
|
+
|
|
643
|
+
// Test that it doesn't throw when called
|
|
644
|
+
expect(() => Api.send('test-id', 'test-process', 'running', 'Test message')).not.toThrow()
|
|
645
|
+
})
|
|
646
|
+
})
|
|
647
|
+
})
|