@@ -564,6 +564,142 @@ func makeCfgWithRoot(t *testing.T, root string) *config.Config {
564564 return cfg
565565}
566566
567+ // ---------------------------------------------------------------------------
568+ // Embedded default asset fallback tests
569+ // ---------------------------------------------------------------------------
570+
571+ // setupEmptyRootCfg creates a config whose files.root is an empty temp dir,
572+ // so every disk lookup will miss and trigger the embedded fallback path.
573+ func setupEmptyRootCfg (t * testing.T ) * config.Config {
574+ t .Helper ()
575+ root := t .TempDir () // intentionally empty
576+ cfg := makeCfgWithRoot (t , root )
577+ cfg .Compression .Enabled = false // keep responses simple for content checks
578+ return cfg
579+ }
580+
581+ // TestEmbedFallback_IndexHTML verifies that a GET / against an empty root
582+ // returns the embedded index.html with status 200 and HTML content.
583+ func TestEmbedFallback_IndexHTML (t * testing.T ) {
584+ cfg := setupEmptyRootCfg (t )
585+ c := cache .NewCache (cfg .Cache .MaxBytes )
586+ h := handler .BuildHandler (cfg , c )
587+
588+ req := httptest .NewRequest (http .MethodGet , "/" , nil )
589+ rr := httptest .NewRecorder ()
590+ h .ServeHTTP (rr , req )
591+
592+ if rr .Code != http .StatusOK {
593+ t .Errorf ("status = %d, want 200 for embedded index.html" , rr .Code )
594+ }
595+ if ct := rr .Header ().Get ("Content-Type" ); ! strings .Contains (ct , "text/html" ) {
596+ t .Errorf ("Content-Type = %q, want text/html for embedded index.html" , ct )
597+ }
598+ if body := rr .Body .String (); ! strings .Contains (body , "<html" ) {
599+ t .Errorf ("embedded index.html body does not look like HTML: %q" , body [:min (len (body ), 120 )])
600+ }
601+ }
602+
603+ // TestEmbedFallback_StyleCSS verifies that /style.css is served from the
604+ // embedded FS when the file is absent from files.root.
605+ func TestEmbedFallback_StyleCSS (t * testing.T ) {
606+ cfg := setupEmptyRootCfg (t )
607+ c := cache .NewCache (cfg .Cache .MaxBytes )
608+ h := handler .BuildHandler (cfg , c )
609+
610+ req := httptest .NewRequest (http .MethodGet , "/style.css" , nil )
611+ rr := httptest .NewRecorder ()
612+ h .ServeHTTP (rr , req )
613+
614+ if rr .Code != http .StatusOK {
615+ t .Errorf ("status = %d, want 200 for embedded style.css" , rr .Code )
616+ }
617+ if ct := rr .Header ().Get ("Content-Type" ); ! strings .Contains (ct , "text/css" ) {
618+ t .Errorf ("Content-Type = %q, want text/css for embedded style.css" , ct )
619+ }
620+ if rr .Body .Len () == 0 {
621+ t .Error ("embedded style.css response body must not be empty" )
622+ }
623+ }
624+
625+ // TestEmbedFallback_404HTML verifies that a truly unknown file (not in the
626+ // embedded FS either) falls all the way through to serveNotFound, which itself
627+ // serves the embedded 404.html with status 404.
628+ func TestEmbedFallback_404HTML (t * testing.T ) {
629+ cfg := setupEmptyRootCfg (t )
630+ cfg .Files .NotFound = "" // no custom 404 configured
631+ c := cache .NewCache (cfg .Cache .MaxBytes )
632+ h := handler .BuildHandler (cfg , c )
633+
634+ req := httptest .NewRequest (http .MethodGet , "/totally-unknown-file.xyz" , nil )
635+ rr := httptest .NewRecorder ()
636+ h .ServeHTTP (rr , req )
637+
638+ if rr .Code != http .StatusNotFound {
639+ t .Errorf ("status = %d, want 404 for unknown file" , rr .Code )
640+ }
641+ if ct := rr .Header ().Get ("Content-Type" ); ! strings .Contains (ct , "text/html" ) {
642+ t .Errorf ("Content-Type = %q, want text/html for embedded 404 page" , ct )
643+ }
644+ if body := rr .Body .String (); ! strings .Contains (body , "<html" ) {
645+ t .Errorf ("embedded 404.html body does not look like HTML: %q" , body [:min (len (body ), 120 )])
646+ }
647+ }
648+
649+ // TestEmbedFallback_SubpathNotServed verifies that the embed fallback only
650+ // handles flat filenames. A URL like /sub/index.html must NOT be served from
651+ // the embedded FS (guard against sub-path traversal) and must return 404.
652+ func TestEmbedFallback_SubpathNotServed (t * testing.T ) {
653+ cfg := setupEmptyRootCfg (t )
654+ cfg .Files .NotFound = ""
655+ c := cache .NewCache (cfg .Cache .MaxBytes )
656+ h := handler .BuildHandler (cfg , c )
657+
658+ req := httptest .NewRequest (http .MethodGet , "/sub/index.html" , nil )
659+ rr := httptest .NewRecorder ()
660+ h .ServeHTTP (rr , req )
661+
662+ if rr .Code != http .StatusNotFound {
663+ t .Errorf ("status = %d, want 404: embed fallback must not serve sub-path URLs" , rr .Code )
664+ }
665+ }
666+
667+ // TestEmbedFallback_CustomNotFoundTakesPriority verifies that a configured
668+ // files.not_found disk file is still preferred over the embedded 404.html.
669+ func TestEmbedFallback_CustomNotFoundTakesPriority (t * testing.T ) {
670+ root := t .TempDir ()
671+ // Write a custom 404 page to disk (but no other files).
672+ custom404 := "<h1>My Custom 404</h1>"
673+ if err := os .WriteFile (filepath .Join (root , "my404.html" ), []byte (custom404 ), 0644 ); err != nil {
674+ t .Fatal (err )
675+ }
676+
677+ cfg := makeCfgWithRoot (t , root )
678+ cfg .Files .NotFound = "my404.html"
679+ cfg .Compression .Enabled = false
680+ c := cache .NewCache (cfg .Cache .MaxBytes )
681+ h := handler .BuildHandler (cfg , c )
682+
683+ req := httptest .NewRequest (http .MethodGet , "/missing.html" , nil )
684+ rr := httptest .NewRecorder ()
685+ h .ServeHTTP (rr , req )
686+
687+ if rr .Code != http .StatusNotFound {
688+ t .Errorf ("status = %d, want 404" , rr .Code )
689+ }
690+ if ! strings .Contains (rr .Body .String (), "My Custom 404" ) {
691+ t .Errorf ("expected custom 404 page to take priority over embedded one, got: %q" , rr .Body .String ())
692+ }
693+ }
694+
695+ // min is a local helper for Go versions that lack the built-in min (pre-1.21).
696+ func min (a , b int ) int {
697+ if a < b {
698+ return a
699+ }
700+ return b
701+ }
702+
567703// ---------------------------------------------------------------------------
568704// Benchmarks
569705// ---------------------------------------------------------------------------
0 commit comments