File size: 3,962 Bytes
8d3471e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package webui

import (
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// TestServeFromDiskPinsContentType ensures static admin assets are returned
// with an explicit, RFC-compliant Content-Type that does not depend on
// mime.TypeByExtension. On Windows mime.TypeByExtension consults the registry
// (HKEY_CLASSES_ROOT) which third-party software can corrupt — for example
// installing certain editors rewrites .css to application/xml — and Chrome
// then refuses to apply a stylesheet whose Content-Type is not text/css,
// breaking the /admin page entirely. Pinning the type by file extension makes
// the response deterministic across operating systems and machine state.
func TestServeFromDiskPinsContentType(t *testing.T) {
	staticDir := t.TempDir()
	assetsDir := filepath.Join(staticDir, "assets")
	if err := os.MkdirAll(assetsDir, 0o755); err != nil {
		t.Fatalf("mkdir assets: %v", err)
	}

	files := map[string]string{
		"index.html":           "<!doctype html><html></html>",
		"assets/index.css":     "body{}",
		"assets/index.js":      "console.log(1)",
		"assets/icon.svg":      `<svg xmlns="http://www.w3.org/2000/svg"></svg>`,
		"assets/source.js.map": `{"version":3}`,
	}
	for rel, body := range files {
		full := filepath.Join(staticDir, filepath.FromSlash(rel))
		if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
			t.Fatalf("mkdir %s: %v", rel, err)
		}
		if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
			t.Fatalf("write %s: %v", rel, err)
		}
	}

	h := &Handler{StaticDir: staticDir}

	cases := []struct {
		urlPath      string
		wantPrefix   string
		wantCacheCtl string
	}{
		{"/admin/assets/index.css", "text/css", "public, max-age=31536000, immutable"},
		{"/admin/assets/index.js", "text/javascript", "public, max-age=31536000, immutable"},
		{"/admin/assets/icon.svg", "image/svg+xml", "public, max-age=31536000, immutable"},
		{"/admin/assets/source.js.map", "application/json", "public, max-age=31536000, immutable"},
		// "/admin/index.html" is intentionally omitted: http.ServeFile redirects
		// requests for index.html to "./", matching Go's net/http behavior. The
		// route the SPA actually lands on is "/admin/" below.
		{"/admin/", "text/html", "no-store, must-revalidate"},
	}

	for _, tc := range cases {
		t.Run(tc.urlPath, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, tc.urlPath, nil)
			rec := httptest.NewRecorder()
			h.serveFromDisk(rec, req, staticDir)

			if rec.Code != http.StatusOK {
				t.Fatalf("status = %d, want 200", rec.Code)
			}
			ct := rec.Header().Get("Content-Type")
			if !strings.HasPrefix(ct, tc.wantPrefix) {
				t.Fatalf("Content-Type = %q, want prefix %q", ct, tc.wantPrefix)
			}
			if got := rec.Header().Get("Cache-Control"); got != tc.wantCacheCtl {
				t.Fatalf("Cache-Control = %q, want %q", got, tc.wantCacheCtl)
			}
		})
	}
}

// TestSetStaticContentTypeUnknownExtensionFallsThrough verifies that unknown
// extensions leave the Content-Type header unset, so http.ServeFile can apply
// its own detection (sniffing or mime.TypeByExtension) for cases the pinned
// table does not cover.
func TestSetStaticContentTypeUnknownExtensionFallsThrough(t *testing.T) {
	rec := httptest.NewRecorder()
	setStaticContentType(rec, "/tmp/data.unknownext")
	if got := rec.Header().Get("Content-Type"); got != "" {
		t.Fatalf("Content-Type = %q, want empty for unknown extension", got)
	}
}

// TestSetStaticContentTypeIsCaseInsensitive guards against a regression where
// uppercase extensions (e.g. STYLE.CSS shipped from some build pipelines)
// would bypass the pinned table and fall back to the registry on Windows.
func TestSetStaticContentTypeIsCaseInsensitive(t *testing.T) {
	rec := httptest.NewRecorder()
	setStaticContentType(rec, "/tmp/STYLE.CSS")
	if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, "text/css") {
		t.Fatalf("Content-Type = %q, want text/css prefix", got)
	}
}