@@ -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,255 @@ 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 TestCreateStaticFileHandler_MissingIndexHTML (t * testing.T ) {
521+ logger := log .GetLogger ()
522+ tmpDir := t .TempDir ()
523+
524+ // Create a directory without index.html
525+ otherFile := []byte ("other content" )
526+ requireWriteFile (t , filepath .Join (tmpDir , "other.txt" ), otherFile )
527+
528+ handler := createStaticFileHandler ("/app/" , tmpDir , logger )
529+
530+ t .Run ("returns 404 when serving root without index.html" , func (t * testing.T ) {
531+ req := httptest .NewRequest (http .MethodGet , "/app/" , nil )
532+ rr := httptest .NewRecorder ()
533+
534+ handler .ServeHTTP (rr , req )
535+
536+ // FileServer returns 404 for missing files
537+ assert .Equal (t , http .StatusNotFound , rr .Code )
538+ })
539+
540+ t .Run ("returns 404 when direct index.html request fails" , func (t * testing.T ) {
541+ req := httptest .NewRequest (http .MethodGet , "/app/index.html" , nil )
542+ rr := httptest .NewRecorder ()
543+
544+ handler .ServeHTTP (rr , req )
545+
546+ assert .Equal (t , http .StatusNotFound , rr .Code )
547+ })
548+
549+ t .Run ("returns 404 for SPA fallback when index.html missing" , func (t * testing.T ) {
550+ req := httptest .NewRequest (http .MethodGet , "/app/nonexistent" , nil )
551+ rr := httptest .NewRecorder ()
552+
553+ handler .ServeHTTP (rr , req )
554+
555+ assert .Equal (t , http .StatusNotFound , rr .Code )
556+ })
557+ }
558+
559+ func TestDirectoryExists (t * testing.T ) {
560+ tmpDir := t .TempDir ()
561+
562+ t .Run ("returns true for existing directory" , func (t * testing.T ) {
563+ assert .True (t , directoryExists (tmpDir ))
564+ })
565+
566+ t .Run ("returns false for non-existent directory" , func (t * testing.T ) {
567+ assert .False (t , directoryExists (filepath .Join (tmpDir , "nonexistent" )))
568+ })
569+
570+ t .Run ("returns false for file, not directory" , func (t * testing.T ) {
571+ filePath := filepath .Join (tmpDir , "file.txt" )
572+ requireWriteFile (t , filePath , []byte ("content" ))
573+ assert .False (t , directoryExists (filePath ))
574+ })
575+ }
576+
577+ func TestFileExists (t * testing.T ) {
578+ tmpDir := t .TempDir ()
579+ filePath := filepath .Join (tmpDir , "file.txt" )
580+ requireWriteFile (t , filePath , []byte ("content" ))
581+
582+ t .Run ("returns true for existing file" , func (t * testing.T ) {
583+ assert .True (t , fileExists (filePath ))
584+ })
585+
586+ t .Run ("returns false for non-existent file" , func (t * testing.T ) {
587+ assert .False (t , fileExists (filepath .Join (tmpDir , "nonexistent.txt" )))
588+ })
589+
590+ t .Run ("returns false for directory, not file" , func (t * testing.T ) {
591+ assert .False (t , fileExists (tmpDir ))
592+ })
593+ }
594+
595+ func TestRegisterStaticFileHandlers (t * testing.T ) {
596+ logger := log .GetLogger ()
597+ tmpDir := t .TempDir ()
598+
599+ // Create gate and develop directories
600+ gateDir := filepath .Join (tmpDir , "apps" , "gate" )
601+ developDir := filepath .Join (tmpDir , "apps" , "develop" )
602+ err := os .MkdirAll (gateDir , 0o750 )
603+ assert .NoError (t , err )
604+ err = os .MkdirAll (developDir , 0o750 )
605+ assert .NoError (t , err )
606+
607+ // Create index.html files
608+ requireWriteFile (t , filepath .Join (gateDir , "index.html" ), []byte ("gate app" ))
609+ requireWriteFile (t , filepath .Join (developDir , "index.html" ), []byte ("develop app" ))
610+
611+ t .Run ("registers handlers for existing directories" , func (t * testing.T ) {
612+ mux := http .NewServeMux ()
613+ registerStaticFileHandlers (logger , mux , tmpDir )
614+
615+ // Test gate handler
616+ req := httptest .NewRequest (http .MethodGet , "/gate/" , nil )
617+ rr := httptest .NewRecorder ()
618+ mux .ServeHTTP (rr , req )
619+ assert .Equal (t , http .StatusOK , rr .Code )
620+ assert .Contains (t , rr .Body .String (), "gate app" )
621+
622+ // Test develop handler
623+ req = httptest .NewRequest (http .MethodGet , "/develop/" , nil )
624+ rr = httptest .NewRecorder ()
625+ mux .ServeHTTP (rr , req )
626+ assert .Equal (t , http .StatusOK , rr .Code )
627+ assert .Contains (t , rr .Body .String (), "develop app" )
628+ })
629+
630+ t .Run ("handles missing directories gracefully" , func (t * testing.T ) {
631+ emptyTmpDir := t .TempDir ()
632+ mux := http .NewServeMux ()
633+ // Should not panic
634+ registerStaticFileHandlers (logger , mux , emptyTmpDir )
635+ })
636+ }
637+
388638func requireWriteFile (t * testing.T , path string , content []byte ) {
389639 t .Helper ()
390640 cleanPath := filepath .Clean (path )
0 commit comments