//go:build embed // +build embed package site_test import ( "context" "fmt" "io" "net/http" "net/http/httptest" "testing" "testing/fstest" "time" "github.com/stretchr/testify/require" "github.com/coder/coder/site" ) func TestCaching(t *testing.T) { t.Parallel() // Create a test server rootFS := fstest.MapFS{ "bundle.js": &fstest.MapFile{}, "image.png": &fstest.MapFile{}, "static/image.png": &fstest.MapFile{}, "favicon.ico": &fstest.MapFile{ Data: []byte("folderFile"), }, "service-worker.js": &fstest.MapFile{}, "index.html": &fstest.MapFile{ Data: []byte("folderFile"), }, "terminal.html": &fstest.MapFile{ Data: []byte("folderFile"), }, } srv := httptest.NewServer(site.Handler(rootFS)) defer srv.Close() // Create a context ctx, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second) defer cancelFunc() testCases := []struct { path string isExpectingCache bool }{ {"/bundle.js", true}, {"/image.png", true}, {"/static/image.png", true}, {"/favicon.ico", true}, {"/", false}, {"/service-worker.js", false}, {"/index.html", false}, {"/double/nested/terminal.html", false}, } for _, testCase := range testCases { req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+testCase.path, nil) require.NoError(t, err, "create request") res, err := srv.Client().Do(req) require.NoError(t, err, "get index") cache := res.Header.Get("Cache-Control") if testCase.isExpectingCache { require.Equalf(t, "public, max-age=31536000, immutable", cache, "expected %w file to have immutable cache", testCase.path) } else { require.Equalf(t, "", cache, "expected %w file to not have immutable cache header", testCase.path) } require.NoError(t, res.Body.Close(), "closing response") } } func TestServingFiles(t *testing.T) { t.Parallel() // Create a test server rootFS := fstest.MapFS{ "index.html": &fstest.MapFile{ Data: []byte("index-bytes"), }, "favicon.ico": &fstest.MapFile{ Data: []byte("favicon-bytes"), }, "dashboard.js": &fstest.MapFile{ Data: []byte("dashboard-js-bytes"), }, "dashboard.css": &fstest.MapFile{ Data: []byte("dashboard-css-bytes"), }, } srv := httptest.NewServer(site.Handler(rootFS)) defer srv.Close() // Create a context ctx, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second) defer cancelFunc() var testCases = []struct { path string expected string }{ // Index cases {"/", "index-bytes"}, {"/index.html", "index-bytes"}, {"/nested", "index-bytes"}, {"/nested/", "index-bytes"}, {"/nested/index.html", "index-bytes"}, // These are nested paths that should lead back to index. We don't // allow nested JS or CSS files. {"/double/nested", "index-bytes"}, {"/double/nested/", "index-bytes"}, {"/double/nested/index.html", "index-bytes"}, {"/nested/dashboard.js", "index-bytes"}, {"/nested/dashboard.css", "index-bytes"}, {"/double/nested/dashboard.js", "index-bytes"}, {"/double/nested/dashboard.css", "index-bytes"}, // Favicon cases // The favicon is always root-referenced in index.html: {"/favicon.ico", "favicon-bytes"}, // JS, CSS cases {"/dashboard.js", "dashboard-js-bytes"}, {"/dashboard.css", "dashboard-css-bytes"}, } for _, testCase := range testCases { path := srv.URL + testCase.path req, err := http.NewRequestWithContext(ctx, "GET", path, nil) require.NoError(t, err) resp, err := http.DefaultClient.Do(req) require.NoError(t, err, "get file") data, _ := io.ReadAll(resp.Body) require.Equal(t, string(data), testCase.expected, "Verify file: "+testCase.path) err = resp.Body.Close() require.NoError(t, err) } } func TestShouldCacheFile(t *testing.T) { t.Parallel() var testCases = []struct { reqFile string expected bool }{ {"123456789.js", true}, {"apps/app/code/terminal.css", true}, {"image.png", true}, {"static/image.png", true}, {"static/images/section-a/image.jpeg", true}, {"service-worker.js", false}, {"dashboard.html", false}, {"apps/app/code/terminal.html", false}, } for _, testCase := range testCases { got := site.ShouldCacheFile(testCase.reqFile) require.Equal(t, testCase.expected, got, fmt.Sprintf("Expected ShouldCacheFile(%s) to be %t", testCase.reqFile, testCase.expected)) } }