Files
2026-08-11 10:40:10 +01:00

63 lines
1.9 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// assetHandler mirrors how the router mounts the embedded assets: the request
// path has "/static/" stripped, and the file system is expected to be rooted at
// the assets themselves.
func assetHandler() http.Handler {
return http.StripPrefix("/static/", http.FileServerFS(StaticFS()))
}
// Static assets must actually be served. A 404 here does not look like a
// missing file in the browser: the 404 body arrives as text/plain under
// X-Content-Type-Options: nosniff, so the browser refuses it and reports
// corrupted content, which sends you looking for an encoding bug instead.
func TestStaticAssetsAreServed(t *testing.T) {
h := assetHandler()
for _, asset := range []struct{ path, wantType string }{
{"/static/app.css", "text/css"},
{"/static/public.css", "text/css"},
{"/static/app.js", "javascript"},
{"/static/favicon.svg", "image/svg+xml"},
} {
t.Run(asset.path, func(t *testing.T) {
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, asset.path, nil))
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200 — the asset is not reachable at the path "+
"the templates reference", w.Code)
}
if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, asset.wantType) {
t.Fatalf("Content-Type = %q, want something containing %q", ct, asset.wantType)
}
if w.Body.Len() == 0 {
t.Fatal("the asset is empty")
}
})
}
}
// Path traversal must not escape the asset root.
func TestStaticAssetsRefuseTraversal(t *testing.T) {
h := assetHandler()
for _, p := range []string{
"/static/../render.go",
"/static/../../go.mod",
"/static/..%2f..%2fgo.mod",
} {
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, p, nil))
if w.Code == http.StatusOK {
t.Errorf("%s was served with 200; it must not escape the asset root", p)
}
}
}