@@ -25,6 +25,18 @@ vi.mock('../../../src/db', () => ({
25
25
getSchema : vi . fn ( ) ,
26
26
} ) )
27
27
28
+ // Mock the encryption module
29
+ vi . mock ( '../../../src/utils/encryption' , ( ) => ( {
30
+ encrypt : vi . fn ( ( value ) => `encrypted_${ value } ` ) ,
31
+ } ) )
32
+
33
+ // Mock path module
34
+ vi . mock ( 'path' , ( ) => ( {
35
+ default : {
36
+ join : vi . fn ( ( ...args ) => args . join ( '/' ) ) ,
37
+ }
38
+ } ) )
39
+
28
40
describe ( 'GlobalSettingsInitService' , ( ) => {
29
41
const mockGlobalSettingsService = GlobalSettingsService as any
30
42
@@ -130,6 +142,290 @@ describe('GlobalSettingsInitService', () => {
130
142
// Should throw the file system error
131
143
await expect ( GlobalSettingsInitService . loadSettingsDefinitions ( ) ) . rejects . toThrow ( 'File system error' )
132
144
} )
145
+
146
+ it ( 'should load settings modules from files' , async ( ) => {
147
+ const fs = await import ( 'fs' )
148
+ const mockFs = fs . default as any
149
+
150
+ // Mock file system to return test files
151
+ mockFs . readdirSync . mockReturnValue ( [ 'smtp.ts' , 'global.ts' , 'index.ts' , 'types.ts' , 'helpers.ts' ] )
152
+
153
+ // Mock dynamic imports
154
+ const mockSmtpModule = {
155
+ smtpSettings : {
156
+ group : { id : 'smtp' , name : 'SMTP Settings' , sort_order : 1 } ,
157
+ settings : [
158
+ { key : 'smtp.host' , defaultValue : '' , type : 'string' , description : 'SMTP host' , encrypted : false , required : true }
159
+ ]
160
+ }
161
+ }
162
+
163
+ const mockGlobalModule = {
164
+ globalSettings : {
165
+ group : { id : 'global' , name : 'Global Settings' , sort_order : 0 } ,
166
+ settings : [
167
+ { key : 'global.page_url' , defaultValue : 'http://localhost:5173' , type : 'string' , description : 'Page URL' , encrypted : false , required : false }
168
+ ]
169
+ }
170
+ }
171
+
172
+ // Mock the dynamic import function
173
+ const originalImport = global . __dirname
174
+ vi . stubGlobal ( '__dirname' , '/test/path' )
175
+
176
+ // Mock import calls
177
+ vi . doMock ( '/test/path/smtp.ts' , ( ) => mockSmtpModule )
178
+ vi . doMock ( '/test/path/global.ts' , ( ) => mockGlobalModule )
179
+
180
+ await GlobalSettingsInitService . loadSettingsDefinitions ( )
181
+
182
+ expect ( GlobalSettingsInitService [ 'isLoaded' ] ) . toBe ( true )
183
+ expect ( GlobalSettingsInitService [ 'settingsModules' ] ) . toHaveLength ( 2 )
184
+ } )
185
+
186
+ it ( 'should handle import errors gracefully' , async ( ) => {
187
+ const fs = await import ( 'fs' )
188
+ const mockFs = fs . default as any
189
+
190
+ mockFs . readdirSync . mockReturnValue ( [ 'invalid.ts' ] )
191
+ vi . stubGlobal ( '__dirname' , '/test/path' )
192
+
193
+ // This should not throw, but continue processing
194
+ await expect ( GlobalSettingsInitService . loadSettingsDefinitions ( ) ) . resolves . not . toThrow ( )
195
+ expect ( GlobalSettingsInitService [ 'isLoaded' ] ) . toBe ( true )
196
+ } )
197
+ } )
198
+
199
+ describe ( 'initializeSettings' , ( ) => {
200
+ it ( 'should initialize settings successfully' , async ( ) => {
201
+ // Setup test modules
202
+ GlobalSettingsInitService [ 'settingsModules' ] = [
203
+ {
204
+ group : { id : 'test' , name : 'Test Group' , sort_order : 0 } ,
205
+ settings : [
206
+ { key : 'test.setting1' , defaultValue : 'value1' , type : 'string' , description : 'Test setting' , encrypted : false , required : false }
207
+ ]
208
+ }
209
+ ]
210
+ GlobalSettingsInitService [ 'isLoaded' ] = true
211
+
212
+ mockGlobalSettingsService . exists . mockResolvedValue ( false )
213
+
214
+ const result = await GlobalSettingsInitService . initializeSettings ( )
215
+
216
+ expect ( result . totalModules ) . toBe ( 1 )
217
+ expect ( result . totalSettings ) . toBe ( 1 )
218
+ expect ( result . created ) . toBeGreaterThanOrEqual ( 0 )
219
+ expect ( result . skipped ) . toBeGreaterThanOrEqual ( 0 )
220
+ } )
221
+
222
+ it ( 'should skip existing settings' , async ( ) => {
223
+ GlobalSettingsInitService [ 'settingsModules' ] = [
224
+ {
225
+ group : { id : 'test' , name : 'Test Group' , sort_order : 0 } ,
226
+ settings : [
227
+ { key : 'test.setting1' , defaultValue : 'value1' , type : 'string' , description : 'Test setting' , encrypted : false , required : false }
228
+ ]
229
+ }
230
+ ]
231
+ GlobalSettingsInitService [ 'isLoaded' ] = true
232
+
233
+ mockGlobalSettingsService . exists . mockResolvedValue ( true )
234
+
235
+ const result = await GlobalSettingsInitService . initializeSettings ( )
236
+
237
+ expect ( result . totalModules ) . toBe ( 1 )
238
+ expect ( result . totalSettings ) . toBe ( 1 )
239
+ expect ( result . skipped ) . toBeGreaterThanOrEqual ( 0 )
240
+ } )
241
+
242
+ it ( 'should load settings definitions if not loaded' , async ( ) => {
243
+ GlobalSettingsInitService [ 'isLoaded' ] = false
244
+
245
+ const fs = await import ( 'fs' )
246
+ const mockFs = fs . default as any
247
+ mockFs . readdirSync . mockReturnValue ( [ ] )
248
+
249
+ const result = await GlobalSettingsInitService . initializeSettings ( )
250
+
251
+ expect ( GlobalSettingsInitService [ 'isLoaded' ] ) . toBe ( true )
252
+ expect ( result . totalModules ) . toBe ( 0 )
253
+ } )
254
+ } )
255
+
256
+ describe ( 'validateRequiredSettings' , ( ) => {
257
+ beforeEach ( ( ) => {
258
+ GlobalSettingsInitService [ 'settingsModules' ] = [
259
+ {
260
+ group : { id : 'smtp' , name : 'SMTP Settings' , sort_order : 1 } ,
261
+ settings : [
262
+ { key : 'smtp.host' , defaultValue : '' , type : 'string' , description : 'SMTP host' , encrypted : false , required : true } ,
263
+ { key : 'smtp.port' , defaultValue : 587 , type : 'number' , description : 'SMTP port' , encrypted : false , required : true } ,
264
+ { key : 'smtp.from_name' , defaultValue : 'DeployStack' , type : 'string' , description : 'From name' , encrypted : false , required : false }
265
+ ]
266
+ } ,
267
+ {
268
+ group : { id : 'global' , name : 'Global Settings' , sort_order : 0 } ,
269
+ settings : [
270
+ { key : 'global.page_url' , defaultValue : 'http://localhost:5173' , type : 'string' , description : 'Page URL' , encrypted : false , required : true }
271
+ ]
272
+ }
273
+ ]
274
+ GlobalSettingsInitService [ 'isLoaded' ] = true
275
+ } )
276
+
277
+ it ( 'should return valid when all required settings have values' , async ( ) => {
278
+ mockGlobalSettingsService . get
279
+ . mockResolvedValueOnce ( { key : 'smtp.host' , value : 'smtp.example.com' , type : 'string' } )
280
+ . mockResolvedValueOnce ( { key : 'smtp.port' , value : '587' , type : 'number' } )
281
+ . mockResolvedValueOnce ( { key : 'global.page_url' , value : 'https://example.com' , type : 'string' } )
282
+
283
+ const result = await GlobalSettingsInitService . validateRequiredSettings ( )
284
+
285
+ expect ( result . valid ) . toBe ( true )
286
+ expect ( result . missing ) . toEqual ( [ ] )
287
+ expect ( result . groups . smtp . missing ) . toBe ( 0 )
288
+ expect ( result . groups . global . missing ) . toBe ( 0 )
289
+ } )
290
+
291
+ it ( 'should return invalid when required settings are missing' , async ( ) => {
292
+ mockGlobalSettingsService . get
293
+ . mockResolvedValueOnce ( null ) // smtp.host missing
294
+ . mockResolvedValueOnce ( { key : 'smtp.port' , value : '587' , type : 'number' } )
295
+ . mockResolvedValueOnce ( { key : 'global.page_url' , value : '' , type : 'string' } ) // empty value
296
+
297
+ const result = await GlobalSettingsInitService . validateRequiredSettings ( )
298
+
299
+ expect ( result . valid ) . toBe ( false )
300
+ expect ( result . missing ) . toEqual ( [ 'smtp.host' , 'global.page_url' ] )
301
+ expect ( result . groups . smtp . missing ) . toBe ( 1 )
302
+ expect ( result . groups . smtp . missingKeys ) . toEqual ( [ 'smtp.host' ] )
303
+ expect ( result . groups . global . missing ) . toBe ( 1 )
304
+ expect ( result . groups . global . missingKeys ) . toEqual ( [ 'global.page_url' ] )
305
+ } )
306
+
307
+ it ( 'should handle database errors gracefully' , async ( ) => {
308
+ mockGlobalSettingsService . get . mockRejectedValue ( new Error ( 'Database error' ) )
309
+
310
+ const result = await GlobalSettingsInitService . validateRequiredSettings ( )
311
+
312
+ expect ( result . valid ) . toBe ( false )
313
+ expect ( result . missing ) . toEqual ( [ 'smtp.host' , 'smtp.port' , 'global.page_url' ] )
314
+ } )
315
+
316
+ it ( 'should load settings definitions if not loaded' , async ( ) => {
317
+ // Reset state completely for this test
318
+ GlobalSettingsInitService [ 'isLoaded' ] = false
319
+ GlobalSettingsInitService [ 'settingsModules' ] = [ ]
320
+
321
+ const fs = await import ( 'fs' )
322
+ const mockFs = fs . default as any
323
+ mockFs . readdirSync . mockReturnValue ( [ ] )
324
+
325
+ const result = await GlobalSettingsInitService . validateRequiredSettings ( )
326
+
327
+ expect ( GlobalSettingsInitService [ 'isLoaded' ] ) . toBe ( true )
328
+ expect ( result . missing ) . toEqual ( [ ] ) // No required settings when no modules loaded
329
+ expect ( Object . keys ( result . groups ) ) . toEqual ( [ ] ) // No groups when no modules loaded
330
+ } )
331
+ } )
332
+
333
+ describe ( 'helper methods' , ( ) => {
334
+ describe ( 'isGitHubOAuthConfigured' , ( ) => {
335
+ it ( 'should return true when GitHub OAuth is configured and enabled' , async ( ) => {
336
+ mockGlobalSettingsService . get
337
+ . mockResolvedValueOnce ( { key : 'github.oauth.client_id' , value : 'client123' , type : 'string' } )
338
+ . mockResolvedValueOnce ( { key : 'github.oauth.client_secret' , value : 'secret456' , type : 'string' } )
339
+ . mockResolvedValueOnce ( { key : 'github.oauth.enabled' , value : 'true' , type : 'boolean' } )
340
+ . mockResolvedValueOnce ( { key : 'github.oauth.callback_url' , value : 'http://localhost:3000/callback' , type : 'string' } )
341
+ . mockResolvedValueOnce ( { key : 'github.oauth.scope' , value : 'user:email' , type : 'string' } )
342
+
343
+ const result = await GlobalSettingsInitService . isGitHubOAuthConfigured ( )
344
+ expect ( result ) . toBe ( true )
345
+ } )
346
+
347
+ it ( 'should return false when GitHub OAuth is not configured' , async ( ) => {
348
+ mockGlobalSettingsService . get . mockResolvedValue ( null )
349
+
350
+ const result = await GlobalSettingsInitService . isGitHubOAuthConfigured ( )
351
+ expect ( result ) . toBe ( false )
352
+ } )
353
+ } )
354
+
355
+ describe ( 'isEmailRegistrationEnabled' , ( ) => {
356
+ it ( 'should return true when email registration is enabled' , async ( ) => {
357
+ mockGlobalSettingsService . get . mockResolvedValue ( {
358
+ key : 'global.enable_email_registration' ,
359
+ value : 'true' ,
360
+ type : 'boolean'
361
+ } )
362
+
363
+ const result = await GlobalSettingsInitService . isEmailRegistrationEnabled ( )
364
+ expect ( result ) . toBe ( true )
365
+ } )
366
+
367
+ it ( 'should return false when email registration is disabled' , async ( ) => {
368
+ mockGlobalSettingsService . get . mockResolvedValue ( {
369
+ key : 'global.enable_email_registration' ,
370
+ value : 'false' ,
371
+ type : 'boolean'
372
+ } )
373
+
374
+ const result = await GlobalSettingsInitService . isEmailRegistrationEnabled ( )
375
+ expect ( result ) . toBe ( false )
376
+ } )
377
+
378
+ it ( 'should return false when setting does not exist' , async ( ) => {
379
+ mockGlobalSettingsService . get . mockResolvedValue ( null )
380
+
381
+ const result = await GlobalSettingsInitService . isEmailRegistrationEnabled ( )
382
+ expect ( result ) . toBe ( false ) // null?.value === 'true' is false
383
+ } )
384
+ } )
385
+ } )
386
+
387
+ describe ( 'error handling in configuration getters' , ( ) => {
388
+ it ( 'should handle errors in getSmtpConfiguration' , async ( ) => {
389
+ mockGlobalSettingsService . get . mockRejectedValue ( new Error ( 'Database error' ) )
390
+
391
+ const config = await GlobalSettingsInitService . getSmtpConfiguration ( )
392
+ expect ( config ) . toBeNull ( )
393
+ } )
394
+
395
+ it ( 'should handle errors in getGitHubOAuthConfiguration' , async ( ) => {
396
+ mockGlobalSettingsService . get . mockRejectedValue ( new Error ( 'Database error' ) )
397
+
398
+ const config = await GlobalSettingsInitService . getGitHubOAuthConfiguration ( )
399
+ expect ( config ) . toBeNull ( )
400
+ } )
401
+
402
+ it ( 'should handle errors in getGlobalConfiguration' , async ( ) => {
403
+ mockGlobalSettingsService . get . mockRejectedValue ( new Error ( 'Database error' ) )
404
+
405
+ const config = await GlobalSettingsInitService . getGlobalConfiguration ( )
406
+ expect ( config ) . toBeNull ( )
407
+ } )
408
+
409
+ it ( 'should handle errors in isEmailSendingEnabled' , async ( ) => {
410
+ mockGlobalSettingsService . get . mockRejectedValue ( new Error ( 'Database error' ) )
411
+
412
+ const result = await GlobalSettingsInitService . isEmailSendingEnabled ( )
413
+ expect ( result ) . toBe ( false )
414
+ } )
415
+
416
+ it ( 'should handle errors in isLoginEnabled' , async ( ) => {
417
+ mockGlobalSettingsService . get . mockRejectedValue ( new Error ( 'Database error' ) )
418
+
419
+ const result = await GlobalSettingsInitService . isLoginEnabled ( )
420
+ expect ( result ) . toBe ( true ) // Default to enabled on error
421
+ } )
422
+
423
+ it ( 'should handle errors in getPageUrl' , async ( ) => {
424
+ mockGlobalSettingsService . get . mockRejectedValue ( new Error ( 'Database error' ) )
425
+
426
+ const result = await GlobalSettingsInitService . getPageUrl ( )
427
+ expect ( result ) . toBe ( 'http://localhost:5173' ) // Default fallback
428
+ } )
133
429
} )
134
430
135
431
describe ( 'configuration getters' , ( ) => {
0 commit comments