@@ -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,136 @@ 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 .CacheControlNoCache + ", " + constants .CacheControlNoStore + ", " + constants .CacheControlMustRevalidate , 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 .CacheControlNoCache + ", " + constants .CacheControlNoStore + ", " + constants .CacheControlMustRevalidate , 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 .CacheControlNoCache + ", " + constants .CacheControlNoStore + ", " + constants .CacheControlMustRevalidate , rr .Header ().Get (constants .CacheControlHeaderName ),
457+ "Cache-Control header should prevent caching for SPA fallback at %s" , tc .path )
458+ assert .Equal (t , constants .PragmaNoCache , rr .Header ().Get (constants .PragmaHeaderName ),
459+ "Pragma header should be set for SPA fallback at %s" , tc .path )
460+ assert .Equal (t , constants .ExpiresZero , rr .Header ().Get (constants .ExpiresHeaderName ),
461+ "Expires header should be set for SPA fallback at %s" , tc .path )
462+ assert .Contains (t , rr .Body .String (), "index" ,
463+ "Should serve index.html content for SPA fallback at %s" , tc .path )
464+ })
465+ }
466+ })
467+
468+ t .Run ("does not set cache headers for static assets" , func (t * testing.T ) {
469+ testCases := []struct {
470+ path string
471+ description string
472+ content []byte
473+ }{
474+ {"/app/app.js" , "JavaScript file" , jsContent },
475+ {"/app/styles.css" , "CSS file" , cssContent },
476+ {"/app/logo.png" , "image file" , imageContent },
477+ }
478+
479+ for _ , tc := range testCases {
480+ t .Run (tc .description , func (t * testing.T ) {
481+ req := httptest .NewRequest (http .MethodGet , tc .path , nil )
482+ rr := httptest .NewRecorder ()
483+
484+ handler .ServeHTTP (rr , req )
485+
486+ assert .Equal (t , http .StatusOK , rr .Code )
487+ assert .Empty (t , rr .Header ().Get (constants .CacheControlHeaderName ),
488+ "Cache-Control header should not be set for %s" , tc .description )
489+ assert .Empty (t , rr .Header ().Get (constants .PragmaHeaderName ),
490+ "Pragma header should not be set for %s" , tc .description )
491+ assert .Empty (t , rr .Header ().Get (constants .ExpiresHeaderName ),
492+ "Expires header should not be set for %s" , tc .description )
493+ assert .Equal (t , string (tc .content ), rr .Body .String (),
494+ "Should serve correct content for %s" , tc .description )
495+ })
496+ }
497+ })
498+
499+ t .Run ("does not match files ending with index.html incorrectly" , func (t * testing.T ) {
500+ customIndexFile := []byte ("custom index content" )
501+ requireWriteFile (t , filepath .Join (tmpDir , "my-custom-index.html" ), customIndexFile )
502+
503+ req := httptest .NewRequest (http .MethodGet , "/app/my-custom-index.html" , nil )
504+ rr := httptest .NewRecorder ()
505+
506+ handler .ServeHTTP (rr , req )
507+
508+ assert .Equal (t , http .StatusOK , rr .Code )
509+ assert .Empty (t , rr .Header ().Get (constants .CacheControlHeaderName ),
510+ "Cache-Control should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
511+ assert .Empty (t , rr .Header ().Get (constants .PragmaHeaderName ),
512+ "Pragma should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
513+ assert .Empty (t , rr .Header ().Get (constants .ExpiresHeaderName ),
514+ "Expires should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
515+ assert .Equal (t , string (customIndexFile ), rr .Body .String ())
516+ })
517+ }
518+
388519func requireWriteFile (t * testing.T , path string , content []byte ) {
389520 t .Helper ()
390521 cleanPath := filepath .Clean (path )
0 commit comments