@@ -25,6 +25,18 @@ vi.mock('../../../src/db', () => ({
2525 getSchema : vi . fn ( ) ,
2626} ) )
2727
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+
2840describe ( 'GlobalSettingsInitService' , ( ) => {
2941 const mockGlobalSettingsService = GlobalSettingsService as any
3042
@@ -130,6 +142,290 @@ describe('GlobalSettingsInitService', () => {
130142 // Should throw the file system error
131143 await expect ( GlobalSettingsInitService . loadSettingsDefinitions ( ) ) . rejects . toThrow ( 'File system error' )
132144 } )
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+ } )
133429 } )
134430
135431 describe ( 'configuration getters' , ( ) => {
0 commit comments