odac 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. package/web/view/header/main.html +14 -0
@@ -0,0 +1,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
+ })