@@ -385,6 +385,140 @@ func TestCreateStaticFileHandler(t *testing.T) {
385385 })
386386}
387387
388+ func TestCreateStaticFileHandler_CacheHeaders (t * testing.T ) {
389+ logger := log .GetLogger ()
390+ tmpDir := t .TempDir ()
391+
392+ indexContent := []byte ("<!DOCTYPE html><html><body>index</body></html>" )
393+ jsContent := []byte ("console.log('hello');" )
394+ cssContent := []byte ("body { margin: 0; }" )
395+ imageContent := []byte {0xFF , 0xD8 , 0xFF } // Mock image bytes
396+
397+ requireWriteFile (t , filepath .Join (tmpDir , "index.html" ), indexContent )
398+ requireWriteFile (t , filepath .Join (tmpDir , "app.js" ), jsContent )
399+ requireWriteFile (t , filepath .Join (tmpDir , "styles.css" ), cssContent )
400+ requireWriteFile (t , filepath .Join (tmpDir , "logo.png" ), imageContent )
401+
402+ handler := createStaticFileHandler ("/app/" , tmpDir , logger )
403+
404+ expectedCacheControl := "no-cache, no-store, must-revalidate"
405+ expectedPragma := "no-cache"
406+ expectedExpires := "0"
407+
408+ t .Run ("sets cache headers when serving index.html at root" , func (t * testing.T ) {
409+ req := httptest .NewRequest (http .MethodGet , "/app/" , nil )
410+ rr := httptest .NewRecorder ()
411+
412+ handler .ServeHTTP (rr , req )
413+
414+ assert .Equal (t , http .StatusOK , rr .Code )
415+ assert .Equal (t , expectedCacheControl , rr .Header ().Get ("Cache-Control" ),
416+ "Cache-Control header should prevent caching for index.html at root" )
417+ assert .Equal (t , expectedPragma , rr .Header ().Get ("Pragma" ),
418+ "Pragma header should be set for index.html at root" )
419+ assert .Equal (t , expectedExpires , rr .Header ().Get ("Expires" ),
420+ "Expires header should be set for index.html at root" )
421+ assert .Contains (t , rr .Body .String (), "index" )
422+ })
423+
424+ t .Run ("sets cache headers when serving index.html directly" , func (t * testing.T ) {
425+ req := httptest .NewRequest (http .MethodGet , "/app/index.html" , nil )
426+ rr := httptest .NewRecorder ()
427+
428+ handler .ServeHTTP (rr , req )
429+
430+ assert .Equal (t , http .StatusOK , rr .Code )
431+ assert .Equal (t , expectedCacheControl , rr .Header ().Get ("Cache-Control" ),
432+ "Cache-Control header should prevent caching for direct index.html request" )
433+ assert .Equal (t , expectedPragma , rr .Header ().Get ("Pragma" ),
434+ "Pragma header should be set for direct index.html request" )
435+ assert .Equal (t , expectedExpires , rr .Header ().Get ("Expires" ),
436+ "Expires header should be set for direct index.html request" )
437+ assert .Contains (t , rr .Body .String (), "index" )
438+ })
439+
440+ t .Run ("sets cache headers when serving index.html as SPA fallback" , func (t * testing.T ) {
441+ testCases := []struct {
442+ path string
443+ description string
444+ }{
445+ {"/app/dashboard" , "single level path" },
446+ {"/app/users/profile" , "multi level path" },
447+ {"/app/settings/advanced/security" , "deeply nested path" },
448+ {"/app/nonexistent.html" , "non-existent HTML file" },
449+ }
450+
451+ for _ , tc := range testCases {
452+ t .Run (tc .description , func (t * testing.T ) {
453+ req := httptest .NewRequest (http .MethodGet , tc .path , nil )
454+ rr := httptest .NewRecorder ()
455+
456+ handler .ServeHTTP (rr , req )
457+
458+ assert .Equal (t , http .StatusOK , rr .Code )
459+ assert .Equal (t , expectedCacheControl , rr .Header ().Get ("Cache-Control" ),
460+ "Cache-Control header should prevent caching for SPA fallback at %s" , tc .path )
461+ assert .Equal (t , expectedPragma , rr .Header ().Get ("Pragma" ),
462+ "Pragma header should be set for SPA fallback at %s" , tc .path )
463+ assert .Equal (t , expectedExpires , rr .Header ().Get ("Expires" ),
464+ "Expires header should be set for SPA fallback at %s" , tc .path )
465+ assert .Contains (t , rr .Body .String (), "index" ,
466+ "Should serve index.html content for SPA fallback at %s" , tc .path )
467+ })
468+ }
469+ })
470+
471+ t .Run ("does not set cache headers for static assets" , func (t * testing.T ) {
472+ testCases := []struct {
473+ path string
474+ description string
475+ content []byte
476+ }{
477+ {"/app/app.js" , "JavaScript file" , jsContent },
478+ {"/app/styles.css" , "CSS file" , cssContent },
479+ {"/app/logo.png" , "image file" , imageContent },
480+ }
481+
482+ for _ , tc := range testCases {
483+ t .Run (tc .description , func (t * testing.T ) {
484+ req := httptest .NewRequest (http .MethodGet , tc .path , nil )
485+ rr := httptest .NewRecorder ()
486+
487+ handler .ServeHTTP (rr , req )
488+
489+ assert .Equal (t , http .StatusOK , rr .Code )
490+ assert .Empty (t , rr .Header ().Get ("Cache-Control" ),
491+ "Cache-Control header should not be set for %s" , tc .description )
492+ assert .Empty (t , rr .Header ().Get ("Pragma" ),
493+ "Pragma header should not be set for %s" , tc .description )
494+ assert .Empty (t , rr .Header ().Get ("Expires" ),
495+ "Expires header should not be set for %s" , tc .description )
496+ assert .Equal (t , string (tc .content ), rr .Body .String (),
497+ "Should serve correct content for %s" , tc .description )
498+ })
499+ }
500+ })
501+
502+ t .Run ("does not match files ending with index.html incorrectly" , func (t * testing.T ) {
503+ customIndexFile := []byte ("custom index content" )
504+ requireWriteFile (t , filepath .Join (tmpDir , "my-custom-index.html" ), customIndexFile )
505+
506+ req := httptest .NewRequest (http .MethodGet , "/app/my-custom-index.html" , nil )
507+ rr := httptest .NewRecorder ()
508+
509+ handler .ServeHTTP (rr , req )
510+
511+ assert .Equal (t , http .StatusOK , rr .Code )
512+ assert .Empty (t , rr .Header ().Get ("Cache-Control" ),
513+ "Cache-Control should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
514+ assert .Empty (t , rr .Header ().Get ("Pragma" ),
515+ "Pragma should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
516+ assert .Empty (t , rr .Header ().Get ("Expires" ),
517+ "Expires should not be set for files that contain 'index.html' but are not exactly 'index.html'" )
518+ assert .Equal (t , string (customIndexFile ), rr .Body .String ())
519+ })
520+ }
521+
388522func requireWriteFile (t * testing.T , path string , content []byte ) {
389523 t .Helper ()
390524 cleanPath := filepath .Clean (path )
0 commit comments