@@ -148,6 +148,22 @@ describe('ConfigLoader', () => {
148148
149149 expect ( spy . calledOnce ) . to . be . true ; // Should only emit once
150150 } ) ;
151+
152+ it ( 'should not emit event if configurationSources is disabled' , async ( ) => {
153+ const config = {
154+ configurationSources : {
155+ enabled : false ,
156+ } ,
157+ } ;
158+
159+ configLoader = new ConfigLoader ( config ) ;
160+ const spy = sinon . spy ( ) ;
161+ configLoader . on ( 'configurationChanged' , spy ) ;
162+
163+ await configLoader . reloadConfiguration ( ) ;
164+
165+ expect ( spy . called ) . to . be . false ;
166+ } ) ;
151167 } ) ;
152168
153169 describe ( 'initialize' , ( ) => {
@@ -182,6 +198,98 @@ describe('ConfigLoader', () => {
182198 } ) ;
183199 } ) ;
184200
201+ describe ( 'start' , ( ) => {
202+ it ( 'should perform initial load on start if configurationSources is enabled' , async ( ) => {
203+ const mockConfig = {
204+ configurationSources : {
205+ enabled : true ,
206+ sources : [
207+ {
208+ type : 'file' ,
209+ enabled : true ,
210+ path : tempConfigFile ,
211+ } ,
212+ ] ,
213+ reloadIntervalSeconds : 30 ,
214+ } ,
215+ } ;
216+
217+ const configLoader = new ConfigLoader ( mockConfig ) ;
218+ const spy = sinon . spy ( configLoader , 'reloadConfiguration' ) ;
219+ await configLoader . start ( ) ;
220+
221+ expect ( spy . calledOnce ) . to . be . true ;
222+ } ) ;
223+
224+ it ( 'should clear an existing reload interval if it exists' , async ( ) => {
225+ const mockConfig = {
226+ configurationSources : {
227+ enabled : true ,
228+ sources : [
229+ {
230+ type : 'file' ,
231+ enabled : true ,
232+ path : tempConfigFile ,
233+ } ,
234+ ] ,
235+ } ,
236+ } ;
237+
238+ const configLoader = new ConfigLoader ( mockConfig ) ;
239+ configLoader . reloadTimer = setInterval ( ( ) => { } , 1000 ) ;
240+ await configLoader . start ( ) ;
241+
242+ expect ( configLoader . reloadTimer ) . to . be . null ;
243+ } ) ;
244+
245+ it ( 'should run reloadConfiguration multiple times on short reload interval' , async ( ) => {
246+ const mockConfig = {
247+ configurationSources : {
248+ enabled : true ,
249+ sources : [
250+ {
251+ type : 'file' ,
252+ enabled : true ,
253+ path : tempConfigFile ,
254+ } ,
255+ ] ,
256+ reloadIntervalSeconds : 0.01 ,
257+ } ,
258+ } ;
259+
260+ const configLoader = new ConfigLoader ( mockConfig ) ;
261+ const spy = sinon . spy ( configLoader , 'reloadConfiguration' ) ;
262+ await configLoader . start ( ) ;
263+
264+ // Make sure the reload interval is triggered
265+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
266+
267+ expect ( spy . callCount ) . to . greaterThan ( 1 ) ;
268+ } ) ;
269+
270+ it ( 'should clear the interval when stop is called' , async ( ) => {
271+ const mockConfig = {
272+ configurationSources : {
273+ enabled : true ,
274+ sources : [
275+ {
276+ type : 'file' ,
277+ enabled : true ,
278+ path : tempConfigFile ,
279+ } ,
280+ ] ,
281+ } ,
282+ } ;
283+
284+ const configLoader = new ConfigLoader ( mockConfig ) ;
285+ configLoader . reloadTimer = setInterval ( ( ) => { } , 1000 ) ;
286+ expect ( configLoader . reloadTimer ) . to . not . be . null ;
287+
288+ await configLoader . stop ( ) ;
289+ expect ( configLoader . reloadTimer ) . to . be . null ;
290+ } ) ;
291+ } ) ;
292+
185293 describe ( 'loadRemoteConfig' , ( ) => {
186294 let configLoader ;
187295 beforeEach ( async ( ) => {
@@ -205,15 +313,15 @@ describe('ConfigLoader', () => {
205313 enabled : true ,
206314 } ;
207315
208- const config = await configLoader . loadFromGit ( source ) ;
316+ const config = await configLoader . loadFromSource ( source ) ;
209317
210318 // Verify the loaded config has expected structure
211319 expect ( config ) . to . be . an ( 'object' ) ;
212320 expect ( config ) . to . have . property ( 'proxyUrl' ) ;
213321 expect ( config ) . to . have . property ( 'cookieSecret' ) ;
214322 } ) ;
215323
216- it ( 'should throw error for invalid configuration file path' , async function ( ) {
324+ it ( 'should throw error for invalid configuration file path (git) ' , async function ( ) {
217325 const source = {
218326 type : 'git' ,
219327 repository : 'https://github.com/finos/git-proxy.git' ,
@@ -223,13 +331,28 @@ describe('ConfigLoader', () => {
223331 } ;
224332
225333 try {
226- await configLoader . loadFromGit ( source ) ;
334+ await configLoader . loadFromSource ( source ) ;
227335 throw new Error ( 'Expected error was not thrown' ) ;
228336 } catch ( error ) {
229337 expect ( error . message ) . to . equal ( 'Invalid configuration file path in repository' ) ;
230338 }
231339 } ) ;
232340
341+ it ( 'should throw error for invalid configuration file path (file)' , async function ( ) {
342+ const source = {
343+ type : 'file' ,
344+ path : '\0' , // Invalid path
345+ enabled : true ,
346+ } ;
347+
348+ try {
349+ await configLoader . loadFromSource ( source ) ;
350+ throw new Error ( 'Expected error was not thrown' ) ;
351+ } catch ( error ) {
352+ expect ( error . message ) . to . equal ( 'Invalid configuration file path' ) ;
353+ }
354+ } ) ;
355+
233356 it ( 'should load configuration from http' , async function ( ) {
234357 // eslint-disable-next-line no-invalid-this
235358 this . timeout ( 10000 ) ;
@@ -240,13 +363,132 @@ describe('ConfigLoader', () => {
240363 enabled : true ,
241364 } ;
242365
243- const config = await configLoader . loadFromHttp ( source ) ;
366+ const config = await configLoader . loadFromSource ( source ) ;
244367
245368 // Verify the loaded config has expected structure
246369 expect ( config ) . to . be . an ( 'object' ) ;
247370 expect ( config ) . to . have . property ( 'proxyUrl' ) ;
248371 expect ( config ) . to . have . property ( 'cookieSecret' ) ;
249372 } ) ;
373+
374+ it ( 'should throw error if repository is invalid' , async function ( ) {
375+ const source = {
376+ type : 'git' ,
377+ repository : 'invalid-repository' ,
378+ path : 'proxy.config.json' ,
379+ branch : 'main' ,
380+ enabled : true ,
381+ } ;
382+
383+ try {
384+ await configLoader . loadFromSource ( source ) ;
385+ throw new Error ( 'Expected error was not thrown' ) ;
386+ } catch ( error ) {
387+ expect ( error . message ) . to . equal ( 'Invalid repository URL format' ) ;
388+ }
389+ } ) ;
390+
391+ it ( 'should throw error if branch name is invalid' , async function ( ) {
392+ const source = {
393+ type : 'git' ,
394+ repository : 'https://github.com/finos/git-proxy.git' ,
395+ path : 'proxy.config.json' ,
396+ branch : '..' , // invalid branch pattern
397+ enabled : true ,
398+ } ;
399+
400+ try {
401+ await configLoader . loadFromSource ( source ) ;
402+ throw new Error ( 'Expected error was not thrown' ) ;
403+ } catch ( error ) {
404+ expect ( error . message ) . to . equal ( 'Invalid branch name format' ) ;
405+ }
406+ } ) ;
407+
408+ it ( 'should throw error if configuration source is invalid' , async function ( ) {
409+ const source = {
410+ type : 'invalid' ,
411+ repository : 'https://github.com/finos/git-proxy.git' ,
412+ path : 'proxy.config.json' ,
413+ branch : 'main' ,
414+ enabled : true ,
415+ } ;
416+
417+ try {
418+ await configLoader . loadFromSource ( source ) ;
419+ throw new Error ( 'Expected error was not thrown' ) ;
420+ } catch ( error ) {
421+ expect ( error . message ) . to . contain ( 'Unsupported configuration source type' ) ;
422+ }
423+ } ) ;
424+
425+ it ( 'should throw error if repository is a valid URL but not a git repository' , async function ( ) {
426+ const source = {
427+ type : 'git' ,
428+ repository : 'https://github.com/test-org/test-repo.git' ,
429+ path : 'proxy.config.json' ,
430+ branch : 'main' ,
431+ enabled : true ,
432+ } ;
433+
434+ try {
435+ await configLoader . loadFromSource ( source ) ;
436+ throw new Error ( 'Expected error was not thrown' ) ;
437+ } catch ( error ) {
438+ expect ( error . message ) . to . contain ( 'Failed to clone repository' ) ;
439+ }
440+ } ) ;
441+
442+ it ( 'should throw error if repository is a valid git repo but the branch does not exist' , async function ( ) {
443+ const source = {
444+ type : 'git' ,
445+ repository : 'https://github.com/finos/git-proxy.git' ,
446+ path : 'proxy.config.json' ,
447+ branch : 'branch-does-not-exist' ,
448+ enabled : true ,
449+ } ;
450+
451+ try {
452+ await configLoader . loadFromSource ( source ) ;
453+ throw new Error ( 'Expected error was not thrown' ) ;
454+ } catch ( error ) {
455+ expect ( error . message ) . to . contain ( 'Failed to checkout branch' ) ;
456+ }
457+ } ) ;
458+
459+ it ( 'should throw error if config path was not found' , async function ( ) {
460+ const source = {
461+ type : 'git' ,
462+ repository : 'https://github.com/finos/git-proxy.git' ,
463+ path : 'path-not-found.json' ,
464+ branch : 'main' ,
465+ enabled : true ,
466+ } ;
467+
468+ try {
469+ await configLoader . loadFromSource ( source ) ;
470+ throw new Error ( 'Expected error was not thrown' ) ;
471+ } catch ( error ) {
472+ expect ( error . message ) . to . contain ( 'Configuration file not found at' ) ;
473+ }
474+ } ) ;
475+
476+ it ( 'should throw error if config file is not valid JSON' , async function ( ) {
477+ const source = {
478+ type : 'git' ,
479+ repository : 'https://github.com/finos/git-proxy.git' ,
480+ path : 'test/fixtures/baz.js' ,
481+ branch : 'main' ,
482+ enabled : true ,
483+ } ;
484+
485+ try {
486+ await configLoader . loadFromSource ( source ) ;
487+ throw new Error ( 'Expected error was not thrown' ) ;
488+ } catch ( error ) {
489+ expect ( error . message ) . to . contain ( 'Failed to read or parse configuration file' ) ;
490+ }
491+ } ) ;
250492 } ) ;
251493
252494 describe ( 'deepMerge' , ( ) => {
0 commit comments