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,73 @@
|
|
|
1
|
+
const Log = require('../../core/Log')
|
|
2
|
+
|
|
3
|
+
describe('Log', () => {
|
|
4
|
+
let consoleLogSpy
|
|
5
|
+
let consoleErrorSpy
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
9
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
consoleLogSpy.mockRestore()
|
|
14
|
+
consoleErrorSpy.mockRestore()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should initialize with a module prefix', () => {
|
|
18
|
+
const log = new Log()
|
|
19
|
+
const logger = log.init('TestModule')
|
|
20
|
+
logger.log('test message')
|
|
21
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('[TestModule] ', 'test message')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should handle multiple module prefixes', () => {
|
|
25
|
+
const log = new Log()
|
|
26
|
+
const logger = log.init('Module1', 'Module2')
|
|
27
|
+
logger.log('test message')
|
|
28
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('[Module1][Module2] ', 'test message')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should log messages correctly', () => {
|
|
32
|
+
const log = new Log()
|
|
33
|
+
const logger = log.init('Test')
|
|
34
|
+
logger.log('message1', 'message2')
|
|
35
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('[Test] ', 'message1', 'message2')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should log error messages correctly', () => {
|
|
39
|
+
const log = new Log()
|
|
40
|
+
const logger = log.init('Test')
|
|
41
|
+
logger.error('error message')
|
|
42
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('[Test] ', 'error message')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should handle %s format specifiers', () => {
|
|
46
|
+
const log = new Log()
|
|
47
|
+
const logger = log.init('FormatTest')
|
|
48
|
+
logger.log('Hello, %s!', 'World')
|
|
49
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('[FormatTest] ', 'Hello, World!')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should handle multiple %s format specifiers', () => {
|
|
53
|
+
const log = new Log()
|
|
54
|
+
const logger = log.init('FormatTest')
|
|
55
|
+
logger.log('Hello, %s! Welcome, %s.', 'John', 'Jane')
|
|
56
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('[FormatTest] ', 'Hello, John! Welcome, Jane.')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should handle missing arguments for %s', () => {
|
|
60
|
+
const log = new Log()
|
|
61
|
+
const logger = log.init('FormatTest')
|
|
62
|
+
logger.log('Hello, %s!')
|
|
63
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('[FormatTest] ', 'Hello, !')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should not log anything if no arguments are provided to log()', () => {
|
|
67
|
+
const log = new Log()
|
|
68
|
+
const logger = log.init('Test')
|
|
69
|
+
const result = logger.log()
|
|
70
|
+
expect(consoleLogSpy).not.toHaveBeenCalled()
|
|
71
|
+
expect(result).toBeInstanceOf(Log)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Mail.js account management operations
|
|
3
|
+
* Tests mail account CRUD operations with validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {setupGlobalMocks, cleanupGlobalMocks} = require('./__mocks__/testHelpers')
|
|
7
|
+
const {createMockWebsiteConfig, createMockMailAccount} = require('./__mocks__/testFactories')
|
|
8
|
+
|
|
9
|
+
// Mock external dependencies
|
|
10
|
+
jest.mock('bcrypt')
|
|
11
|
+
jest.mock('sqlite3')
|
|
12
|
+
|
|
13
|
+
const bcrypt = require('bcrypt')
|
|
14
|
+
const sqlite3 = require('sqlite3')
|
|
15
|
+
|
|
16
|
+
describe('Mail Module - Account Management Operations', () => {
|
|
17
|
+
let Mail
|
|
18
|
+
let mockDb
|
|
19
|
+
let mockConfig
|
|
20
|
+
let mockApi
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
setupGlobalMocks()
|
|
24
|
+
|
|
25
|
+
// Setup mock database
|
|
26
|
+
mockDb = {
|
|
27
|
+
serialize: jest.fn(callback => callback && callback()),
|
|
28
|
+
run: jest.fn((sql, params, callback) => {
|
|
29
|
+
if (typeof params === 'function') {
|
|
30
|
+
callback = params
|
|
31
|
+
}
|
|
32
|
+
if (callback) callback(null)
|
|
33
|
+
}),
|
|
34
|
+
get: jest.fn((sql, params, callback) => {
|
|
35
|
+
if (typeof params === 'function') {
|
|
36
|
+
callback = params
|
|
37
|
+
params = []
|
|
38
|
+
}
|
|
39
|
+
if (callback) callback(null, null)
|
|
40
|
+
}),
|
|
41
|
+
each: jest.fn((sql, params, rowCallback, completeCallback) => {
|
|
42
|
+
if (typeof params === 'function') {
|
|
43
|
+
completeCallback = rowCallback
|
|
44
|
+
rowCallback = params
|
|
45
|
+
params = []
|
|
46
|
+
}
|
|
47
|
+
if (completeCallback) completeCallback(null, 0)
|
|
48
|
+
}),
|
|
49
|
+
prepare: jest.fn(() => ({
|
|
50
|
+
run: jest.fn(),
|
|
51
|
+
finalize: jest.fn()
|
|
52
|
+
}))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Setup sqlite3 mock
|
|
56
|
+
sqlite3.verbose.mockReturnValue({
|
|
57
|
+
Database: jest.fn().mockImplementation((path, callback) => {
|
|
58
|
+
if (callback) callback(null)
|
|
59
|
+
return mockDb
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Setup mock config
|
|
64
|
+
mockConfig = {
|
|
65
|
+
config: {
|
|
66
|
+
websites: {
|
|
67
|
+
'example.com': createMockWebsiteConfig('example.com', {
|
|
68
|
+
DNS: {
|
|
69
|
+
MX: [{name: 'example.com', value: 'mail.example.com', priority: 10}]
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Setup mock API service
|
|
77
|
+
mockApi = {
|
|
78
|
+
result: jest.fn((success, data) => ({success, data}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Setup global Candy mocks
|
|
82
|
+
global.Candy.setMock('core', 'Config', mockConfig)
|
|
83
|
+
global.Candy.setMock('server', 'Api', mockApi)
|
|
84
|
+
|
|
85
|
+
// Setup bcrypt mock
|
|
86
|
+
bcrypt.hash.mockImplementation((password, rounds, callback) => {
|
|
87
|
+
callback(null, '$2b$10$hashedpassword')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Setup global __ function mock
|
|
91
|
+
global.__ = jest.fn().mockImplementation((key, ...args) => {
|
|
92
|
+
return Promise.resolve(key.replace(/%s/g, () => args.shift() || '%s'))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Clear module cache and require fresh instance
|
|
96
|
+
jest.resetModules()
|
|
97
|
+
Mail = require('../../server/src/Mail')
|
|
98
|
+
|
|
99
|
+
// Initialize Mail module
|
|
100
|
+
Mail.init()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
cleanupGlobalMocks()
|
|
105
|
+
jest.clearAllMocks()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('Account Creation', () => {
|
|
109
|
+
test('should create mail account with valid email and password', async () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
const email = 'newuser@example.com'
|
|
112
|
+
const password = 'testpassword'
|
|
113
|
+
const retype = 'testpassword'
|
|
114
|
+
|
|
115
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
116
|
+
callback(null, null) // Account doesn't exist
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
const result = await Mail.create(email, password, retype)
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
expect(bcrypt.hash).toHaveBeenCalledWith(password, 10, expect.any(Function))
|
|
124
|
+
expect(mockDb.prepare).toHaveBeenCalledWith("INSERT INTO mail_account ('email', 'password', 'domain') VALUES (?, ?, ?)")
|
|
125
|
+
expect(mockApi.result).toHaveBeenCalledWith(true, expect.stringContaining('created successfully'))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('should validate email format during account creation', async () => {
|
|
129
|
+
// Arrange
|
|
130
|
+
const invalidEmail = 'invalid-email'
|
|
131
|
+
const password = 'testpassword'
|
|
132
|
+
const retype = 'testpassword'
|
|
133
|
+
|
|
134
|
+
// Act
|
|
135
|
+
const result = await Mail.create(invalidEmail, password, retype)
|
|
136
|
+
|
|
137
|
+
// Assert
|
|
138
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Invalid email address.')
|
|
139
|
+
expect(bcrypt.hash).not.toHaveBeenCalled()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('should reject account creation if passwords do not match', async () => {
|
|
143
|
+
// Arrange
|
|
144
|
+
const email = 'test@example.com'
|
|
145
|
+
const password = 'password1'
|
|
146
|
+
const retype = 'password2'
|
|
147
|
+
|
|
148
|
+
// Act
|
|
149
|
+
const result = await Mail.create(email, password, retype)
|
|
150
|
+
|
|
151
|
+
// Assert
|
|
152
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Passwords do not match.')
|
|
153
|
+
expect(bcrypt.hash).not.toHaveBeenCalled()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('should reject account creation if required fields are missing', async () => {
|
|
157
|
+
// Act
|
|
158
|
+
const result = await Mail.create('', 'password', 'password')
|
|
159
|
+
|
|
160
|
+
// Assert
|
|
161
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'All fields are required.')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('should reject account creation if account already exists', async () => {
|
|
165
|
+
// Arrange
|
|
166
|
+
const email = 'existing@example.com'
|
|
167
|
+
const password = 'testpassword'
|
|
168
|
+
const retype = 'testpassword'
|
|
169
|
+
|
|
170
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
171
|
+
callback(null, {email: email, password: '$2b$10$hashedpassword'})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Act
|
|
175
|
+
const result = await Mail.create(email, password, retype)
|
|
176
|
+
|
|
177
|
+
// Assert
|
|
178
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, `Mail account ${email} already exists.`)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('should reject account creation for unknown domain', async () => {
|
|
182
|
+
// Arrange
|
|
183
|
+
const email = 'test@unknown.com'
|
|
184
|
+
const password = 'testpassword'
|
|
185
|
+
const retype = 'testpassword'
|
|
186
|
+
|
|
187
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
188
|
+
callback(null, null) // Account doesn't exist
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Act
|
|
192
|
+
const result = await Mail.create(email, password, retype)
|
|
193
|
+
|
|
194
|
+
// Assert
|
|
195
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Domain unknown.com not found.')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('should hash password with bcrypt during account creation', async () => {
|
|
199
|
+
// Arrange
|
|
200
|
+
const email = 'test@example.com'
|
|
201
|
+
const password = 'plainpassword'
|
|
202
|
+
const retype = 'plainpassword'
|
|
203
|
+
|
|
204
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
205
|
+
callback(null, null) // Account doesn't exist
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Act
|
|
209
|
+
await Mail.create(email, password, retype)
|
|
210
|
+
|
|
211
|
+
// Assert
|
|
212
|
+
expect(bcrypt.hash).toHaveBeenCalledWith(password, 10, expect.any(Function))
|
|
213
|
+
|
|
214
|
+
const preparedStatement = mockDb.prepare.mock.results[0].value
|
|
215
|
+
expect(preparedStatement.run).toHaveBeenCalledWith(email, '$2b$10$hashedpassword', 'example.com')
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('Account Deletion', () => {
|
|
220
|
+
test('should delete existing mail account', async () => {
|
|
221
|
+
// Arrange
|
|
222
|
+
const email = 'delete@example.com'
|
|
223
|
+
|
|
224
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
225
|
+
callback(null, {email: email, password: '$2b$10$hashedpassword'})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// Act
|
|
229
|
+
const result = await Mail.delete(email)
|
|
230
|
+
|
|
231
|
+
// Assert
|
|
232
|
+
expect(mockDb.prepare).toHaveBeenCalledWith('DELETE FROM mail_account WHERE email = ?')
|
|
233
|
+
const preparedStatement = mockDb.prepare.mock.results[0].value
|
|
234
|
+
expect(preparedStatement.run).toHaveBeenCalledWith(email)
|
|
235
|
+
expect(mockApi.result).toHaveBeenCalledWith(true, `Mail account ${email} deleted successfully.`)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test('should validate email format during account deletion', async () => {
|
|
239
|
+
// Arrange
|
|
240
|
+
const invalidEmail = 'invalid-email'
|
|
241
|
+
|
|
242
|
+
// Act
|
|
243
|
+
const result = await Mail.delete(invalidEmail)
|
|
244
|
+
|
|
245
|
+
// Assert
|
|
246
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Invalid email address.')
|
|
247
|
+
expect(mockDb.prepare).not.toHaveBeenCalled()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('should reject deletion if email is required but not provided', async () => {
|
|
251
|
+
// Act
|
|
252
|
+
const result = await Mail.delete('')
|
|
253
|
+
|
|
254
|
+
// Assert
|
|
255
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Email address is required.')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('should reject deletion if account does not exist', async () => {
|
|
259
|
+
// Arrange
|
|
260
|
+
const email = 'nonexistent@example.com'
|
|
261
|
+
|
|
262
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
263
|
+
callback(null, null) // Account doesn't exist
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Act
|
|
267
|
+
const result = await Mail.delete(email)
|
|
268
|
+
|
|
269
|
+
// Assert
|
|
270
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, `Mail account ${email} not found.`)
|
|
271
|
+
expect(mockDb.prepare).not.toHaveBeenCalled()
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
describe('Account Existence Checking', () => {
|
|
276
|
+
test('should return account data if account exists', async () => {
|
|
277
|
+
// Arrange
|
|
278
|
+
const email = 'existing@example.com'
|
|
279
|
+
const mockAccount = {email: email, password: '$2b$10$hashedpassword'}
|
|
280
|
+
|
|
281
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
282
|
+
callback(null, mockAccount)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Act
|
|
286
|
+
const result = await Mail.exists(email)
|
|
287
|
+
|
|
288
|
+
// Assert
|
|
289
|
+
expect(result).toEqual(mockAccount)
|
|
290
|
+
expect(mockDb.get).toHaveBeenCalledWith('SELECT * FROM mail_account WHERE email = ?', [email], expect.any(Function))
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('should return false if account does not exist', async () => {
|
|
294
|
+
// Arrange
|
|
295
|
+
const email = 'nonexistent@example.com'
|
|
296
|
+
|
|
297
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
298
|
+
callback(null, null)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// Act
|
|
302
|
+
const result = await Mail.exists(email)
|
|
303
|
+
|
|
304
|
+
// Assert
|
|
305
|
+
expect(result).toBe(false)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('should handle database errors during existence check', async () => {
|
|
309
|
+
// Arrange
|
|
310
|
+
const email = 'test@example.com'
|
|
311
|
+
const dbError = new Error('Database error')
|
|
312
|
+
|
|
313
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
314
|
+
callback(dbError, null)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// Act
|
|
318
|
+
const result = await Mail.exists(email)
|
|
319
|
+
|
|
320
|
+
// Assert
|
|
321
|
+
expect(result).toBe(false)
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe('Password Update', () => {
|
|
326
|
+
test('should update password for existing account', async () => {
|
|
327
|
+
// Arrange
|
|
328
|
+
const email = 'test@example.com'
|
|
329
|
+
const password = 'newpassword'
|
|
330
|
+
const retype = 'newpassword'
|
|
331
|
+
|
|
332
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
333
|
+
callback(null, {email: email, password: '$2b$10$oldhashedpassword'})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// Act
|
|
337
|
+
const result = await Mail.password(email, password, retype)
|
|
338
|
+
|
|
339
|
+
// Assert
|
|
340
|
+
expect(bcrypt.hash).toHaveBeenCalledWith(password, 10, expect.any(Function))
|
|
341
|
+
expect(mockDb.prepare).toHaveBeenCalledWith('UPDATE mail_account SET password = ? WHERE email = ?')
|
|
342
|
+
const preparedStatement = mockDb.prepare.mock.results[0].value
|
|
343
|
+
expect(preparedStatement.run).toHaveBeenCalledWith('$2b$10$hashedpassword', email)
|
|
344
|
+
expect(mockApi.result).toHaveBeenCalledWith(true, `Mail account ${email} password updated successfully.`)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('should validate email format during password update', async () => {
|
|
348
|
+
// Arrange
|
|
349
|
+
const invalidEmail = 'invalid-email'
|
|
350
|
+
const password = 'newpassword'
|
|
351
|
+
const retype = 'newpassword'
|
|
352
|
+
|
|
353
|
+
// Act
|
|
354
|
+
const result = await Mail.password(invalidEmail, password, retype)
|
|
355
|
+
|
|
356
|
+
// Assert
|
|
357
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Invalid email address.')
|
|
358
|
+
expect(bcrypt.hash).not.toHaveBeenCalled()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('should reject password update if passwords do not match', async () => {
|
|
362
|
+
// Arrange
|
|
363
|
+
const email = 'test@example.com'
|
|
364
|
+
const password = 'password1'
|
|
365
|
+
const retype = 'password2'
|
|
366
|
+
|
|
367
|
+
// Act
|
|
368
|
+
const result = await Mail.password(email, password, retype)
|
|
369
|
+
|
|
370
|
+
// Assert
|
|
371
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Passwords do not match.')
|
|
372
|
+
expect(bcrypt.hash).not.toHaveBeenCalled()
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('should reject password update if required fields are missing', async () => {
|
|
376
|
+
// Act
|
|
377
|
+
const result = await Mail.password('', 'password', 'password')
|
|
378
|
+
|
|
379
|
+
// Assert
|
|
380
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'All fields are required.')
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('should reject password update if account does not exist', async () => {
|
|
384
|
+
// Arrange
|
|
385
|
+
const email = 'nonexistent@example.com'
|
|
386
|
+
const password = 'newpassword'
|
|
387
|
+
const retype = 'newpassword'
|
|
388
|
+
|
|
389
|
+
mockDb.get.mockImplementation((sql, params, callback) => {
|
|
390
|
+
callback(null, null) // Account doesn't exist
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// Act
|
|
394
|
+
const result = await Mail.password(email, password, retype)
|
|
395
|
+
|
|
396
|
+
// Assert
|
|
397
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, `Mail account ${email} not found.`)
|
|
398
|
+
expect(mockDb.prepare).not.toHaveBeenCalled()
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
describe('Account Listing', () => {
|
|
403
|
+
test('should list all accounts for a domain', async () => {
|
|
404
|
+
// Arrange
|
|
405
|
+
const domain = 'example.com'
|
|
406
|
+
const mockAccounts = [{email: 'user1@example.com'}, {email: 'user2@example.com'}, {email: 'user3@example.com'}]
|
|
407
|
+
|
|
408
|
+
mockDb.each.mockImplementation((sql, params, rowCallback, completeCallback) => {
|
|
409
|
+
mockAccounts.forEach(account => rowCallback(null, account))
|
|
410
|
+
completeCallback(null, mockAccounts.length)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// Act
|
|
414
|
+
const result = await Mail.list(domain)
|
|
415
|
+
|
|
416
|
+
// Assert
|
|
417
|
+
expect(mockDb.each).toHaveBeenCalledWith(
|
|
418
|
+
'SELECT * FROM mail_account WHERE domain = ?',
|
|
419
|
+
[domain],
|
|
420
|
+
expect.any(Function),
|
|
421
|
+
expect.any(Function)
|
|
422
|
+
)
|
|
423
|
+
expect(mockApi.result).toHaveBeenCalledWith(true, expect.stringContaining('user1@example.com\nuser2@example.com\nuser3@example.com'))
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test('should reject listing if domain is not provided', async () => {
|
|
427
|
+
// Act
|
|
428
|
+
const result = await Mail.list('')
|
|
429
|
+
|
|
430
|
+
// Assert
|
|
431
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Domain is required.')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
test('should reject listing for unknown domain', async () => {
|
|
435
|
+
// Arrange
|
|
436
|
+
const domain = 'unknown.com'
|
|
437
|
+
|
|
438
|
+
// Act
|
|
439
|
+
const result = await Mail.list(domain)
|
|
440
|
+
|
|
441
|
+
// Assert
|
|
442
|
+
expect(mockApi.result).toHaveBeenCalledWith(false, 'Domain unknown.com not found.')
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('should handle empty account list for domain', async () => {
|
|
446
|
+
// Arrange
|
|
447
|
+
const domain = 'example.com'
|
|
448
|
+
|
|
449
|
+
mockDb.each.mockImplementation((sql, params, rowCallback, completeCallback) => {
|
|
450
|
+
completeCallback(null, 0)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// Act
|
|
454
|
+
const result = await Mail.list(domain)
|
|
455
|
+
|
|
456
|
+
// Assert
|
|
457
|
+
expect(mockApi.result).toHaveBeenCalledWith(true, `Mail accounts for domain ${domain}.\n`)
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
})
|