@@ -42,6 +42,7 @@ import (
4242 "github.com/stretchr/testify/suite"
4343
4444 "github.com/asgardeo/thunder/internal/system/config"
45+ "github.com/asgardeo/thunder/internal/system/constants"
4546 "github.com/asgardeo/thunder/internal/system/log"
4647 "github.com/asgardeo/thunder/tests/mocks/jwtmock"
4748)
@@ -385,6 +386,216 @@ func TestCreateStaticFileHandler(t *testing.T) {
385386 })
386387}
387388
389+ func TestCreateStaticFileHandler_CacheHeaders (t * testing.T ) {
390+ logger := log .GetLogger ()
391+ tmpDir := t .TempDir ()
392+
393+ indexContent := []byte ("<!DOCTYPE html><html><body>index</body></html>" )
394+ jsContent := []byte ("console.log('hello');" )
395+ cssContent := []byte ("body { margin: 0; }" )
396+ imageContent := []byte {0xFF , 0xD8 , 0xFF } // Mock image bytes
397+
398+ requireWriteFile (t , filepath .Join (tmpDir , "index.html" ), indexContent )
399+ requireWriteFile (t , filepath .Join (tmpDir , "app.js" ), jsContent )
400+ requireWriteFile (t , filepath .Join (tmpDir , "styles.css" ), cssContent )
401+ requireWriteFile (t , filepath .Join (tmpDir , "logo.png" ), imageContent )
402+
403+ handler := createStaticFileHandler ("/app/" , tmpDir , logger )
404+
405+ t .Run ("sets cache headers when serving index.html at root" , func (t * testing.T ) {
406+ req := httptest .NewRequest (http .MethodGet , "/app/" , nil )
407+ rr := httptest .NewRecorder ()
408+
409+ handler .ServeHTTP (rr , req )
410+
411+ assert .Equal (t , http .StatusOK , rr .Code )
412+ assert .Equal (t , constants .CacheControlNoCacheComposite , rr .Header ().Get (constants .CacheControlHeaderName ),
413+ "Cache-Control header should prevent caching for index.html at root" )
414+ assert .Equal (t , constants .PragmaNoCache , rr .Header ().Get (constants .PragmaHeaderName ),
415+ "Pragma header should be set for index.html at root" )
416+ assert .Equal (t , constants .ExpiresZero , rr .Header ().Get (constants .ExpiresHeaderName ),
417+ "Expires header should be set for index.html at root" )
418+ assert .Contains (t , rr .Body .String (), "index" )
419+ })
420+
421+ t .Run ("sets cache headers when serving index.html directly" , func (t * testing.T ) {
422+ req := httptest .NewRequest (http .MethodGet , "/app/index.html" , nil )
423+ rr := httptest .NewRecorder ()
424+
425+ handler .ServeHTTP (rr , req )
426+
427+ assert .Equal (t , http .StatusOK , rr .Code )
428+ assert .Equal (t , constants .CacheControlNoCacheComposite , rr .Header ().Get (constants .CacheControlHeaderName ),
429+ "Cache-Control header should prevent caching for direct index.html request" )
430+ assert .Equal (t , constants .PragmaNoCache , rr .Header ().Get (constants .PragmaHeaderName ),
431+ "Pragma header should be set for direct index.html request" )
432+ assert .Equal (t , constants .ExpiresZero , rr .Header ().Get (constants .ExpiresHeaderName ),
433+ "Expires header should be set for direct index.html request" )
434+ assert .Contains (t , rr .Body .String (), "index" )
435+ })
436+
437+ t .Run ("sets cache headers when serving index.html as SPA fallback" , func (t * testing.T ) {
438+ testCases := []struct {
439+ path string
440+ description string
441+ }{
442+ {"/app/dashboard" , "single level path" },
443+ {"/app/users/profile" , "multi level path" },
444+ {"/app/settings/advanced/security" , "deeply nested path" },
445+ {"/app/nonexistent.html" , "non-existent HTML file" },
446+ }
447+
448+ for _ , tc := range testCases {
449+ t .Run (tc .description , func (t * testing.T ) {
450+ req := httptest .NewRequest (http .MethodGet , tc .path , nil )
451+ rr := httptest .NewRecorder ()
452+
453+ handler .ServeHTTP (rr , req )
454+
455+ assert .Equal (t , http .StatusOK , rr .Code )
456+ assert .Equal (t , constants .CacheControlNoCacheComposite ,
457+ rr .Header ().Get (constants .CacheControlHeaderName ),
458+ "Cache-Control header should prevent caching for SPA fallback at %s" , tc .path )
459+ assert .Equal (t , constants .PragmaNoCache , rr .Header ().Get (constants .PragmaHeaderName ),
460+ "Pragma header should be set for SPA fallback at %s" , tc .path )
461+ assert .Equal (t , constants .ExpiresZero , rr .Header ().Get (constants .ExpiresHeaderName ),
462+ "Expires header should be set for SPA fallback at %s" , tc .path )
463+ assert .Contains (t , rr .Body .String (), "index" ,
464+ "Should serve index.html content for SPA fallback at %s" , tc .path )
465+ })
466+ }
467+ })
468+
469+ t .Run ("does not set cache headers for static assets" , func (t * testing.T ) {
470+ testCases := []struct {
471+ path string
472+ description string
473+ content []byte
474+ }{
475+ {"/app/app.js" , "JavaScript file" , jsContent },
476+ {"/app/styles.css" , "CSS file" , cssContent },
477+ {"/app/logo.png" , "image file" , imageContent },
478+ }
479+
480+ for _ , tc := range testCases {
481+ t .Run (tc .description , func (t * testing.T ) {
482+ req := httptest .NewRequest (http .MethodGet , tc .path , nil )
483+ rr := httptest .NewRecorder ()
484+
485+ handler .ServeHTTP (rr , req )
486+
487+ assert .Equal (t , http .StatusOK , rr .Code )
488+ assert .Empty (t , rr .Header ().Get (constants .CacheControlHeaderName ),
489+ "Cache-Control header should not be set for %s" , tc .description )
490+ assert .Empty (t , rr .Header ().Get (constants .PragmaHeaderName ),
491+ "Pragma header should not be set for %s" , tc .description )
492+ assert .Empty (t , rr .Header ().Get (constants .ExpiresHeaderName ),
493+ "Expires header should not be set for %s" , tc .description )
494+ assert .Equal (t , string (tc .content ), rr .Body .String (),
495+ "Should serve correct content for %s" , tc .description )
496+ })
497+ }
498+ })
499+
500+ t .Run ("does not match files ending with index.html incorrectly" , func (t * testing.T ) {
501+ customIndexFile := []byte ("custom index content" )
502+ requireWriteFile (t , filepath .Join (tmpDir , "my-custom-index.html" ), customIndexFile )
503+
504+ req := httptest .NewRequest (http .MethodGet , "/app/my-custom-index.html" , nil )
505+ rr := httptest .NewRecorder ()
506+
507+ handler .ServeHTTP (rr , req )
508+
509+ assert .Equal (t , http .StatusOK , rr .Code )
510+ assert .Empty (t , rr .Header ().Get (constants .CacheControlHeaderName ),
511+ "Cache-Control should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
512+ assert .Empty (t , rr .Header ().Get (constants .PragmaHeaderName ),
513+ "Pragma should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
514+ assert .Empty (t , rr .Header ().Get (constants .ExpiresHeaderName ),
515+ "Expires should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
516+ assert .Equal (t , string (customIndexFile ), rr .Body .String ())
517+ })
518+ }
519+
520+ func TestDirectoryExists (t * testing.T ) {
521+ tmpDir := t .TempDir ()
522+
523+ t .Run ("returns true for existing directory" , func (t * testing.T ) {
524+ assert .True (t , directoryExists (tmpDir ))
525+ })
526+
527+ t .Run ("returns false for non-existent directory" , func (t * testing.T ) {
528+ assert .False (t , directoryExists (filepath .Join (tmpDir , "nonexistent" )))
529+ })
530+
531+ t .Run ("returns false for file, not directory" , func (t * testing.T ) {
532+ filePath := filepath .Join (tmpDir , "file.txt" )
533+ requireWriteFile (t , filePath , []byte ("content" ))
534+ assert .False (t , directoryExists (filePath ))
535+ })
536+ }
537+
538+ func TestFileExists (t * testing.T ) {
539+ tmpDir := t .TempDir ()
540+ filePath := filepath .Join (tmpDir , "file.txt" )
541+ requireWriteFile (t , filePath , []byte ("content" ))
542+
543+ t .Run ("returns true for existing file" , func (t * testing.T ) {
544+ assert .True (t , fileExists (filePath ))
545+ })
546+
547+ t .Run ("returns false for non-existent file" , func (t * testing.T ) {
548+ assert .False (t , fileExists (filepath .Join (tmpDir , "nonexistent.txt" )))
549+ })
550+
551+ t .Run ("returns false for directory, not file" , func (t * testing.T ) {
552+ assert .False (t , fileExists (tmpDir ))
553+ })
554+ }
555+
556+ func TestRegisterStaticFileHandlers (t * testing.T ) {
557+ logger := log .GetLogger ()
558+ tmpDir := t .TempDir ()
559+
560+ // Create gate and develop directories
561+ gateDir := filepath .Join (tmpDir , "apps" , "gate" )
562+ developDir := filepath .Join (tmpDir , "apps" , "develop" )
563+ err := os .MkdirAll (gateDir , 0o750 )
564+ assert .NoError (t , err )
565+ err = os .MkdirAll (developDir , 0o750 )
566+ assert .NoError (t , err )
567+
568+ // Create index.html files
569+ requireWriteFile (t , filepath .Join (gateDir , "index.html" ), []byte ("gate app" ))
570+ requireWriteFile (t , filepath .Join (developDir , "index.html" ), []byte ("develop app" ))
571+
572+ t .Run ("registers handlers for existing directories" , func (t * testing.T ) {
573+ mux := http .NewServeMux ()
574+ registerStaticFileHandlers (logger , mux , tmpDir )
575+
576+ // Test gate handler
577+ req := httptest .NewRequest (http .MethodGet , "/gate/" , nil )
578+ rr := httptest .NewRecorder ()
579+ mux .ServeHTTP (rr , req )
580+ assert .Equal (t , http .StatusOK , rr .Code )
581+ assert .Contains (t , rr .Body .String (), "gate app" )
582+
583+ // Test develop handler
584+ req = httptest .NewRequest (http .MethodGet , "/develop/" , nil )
585+ rr = httptest .NewRecorder ()
586+ mux .ServeHTTP (rr , req )
587+ assert .Equal (t , http .StatusOK , rr .Code )
588+ assert .Contains (t , rr .Body .String (), "develop app" )
589+ })
590+
591+ t .Run ("handles missing directories gracefully" , func (t * testing.T ) {
592+ emptyTmpDir := t .TempDir ()
593+ mux := http .NewServeMux ()
594+ // Should not panic
595+ registerStaticFileHandlers (logger , mux , emptyTmpDir )
596+ })
597+ }
598+
388599func requireWriteFile (t * testing.T , path string , content []byte ) {
389600 t .Helper ()
390601 cleanPath := filepath .Clean (path )
0 commit comments