File
Blob: ui/embed_test.go
| 1 | package ui |
| 2 | |
| 3 | import ( |
| 4 | "io/fs" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "net/url" |
| 8 | "regexp" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | ) |
| 12 | |
| 13 | func TestEmbeddedFrontends(t *testing.T) { |
| 14 | for _, app := range []struct { |
| 15 | name string |
| 16 | prefix string |
| 17 | page http.Handler |
| 18 | assets http.Handler |
| 19 | }{ |
| 20 | {"client", "/ui/", ClientPage(), Assets()}, |
| 21 | {"operator", "/_internal/ui/", OperatorPage(), Assets()}, |
| 22 | } { |
| 23 | t.Run(app.name, func(t *testing.T) { |
| 24 | page := httptest.NewRecorder() |
| 25 | app.page.ServeHTTP(page, httptest.NewRequest(http.MethodGet, "/", nil)) |
| 26 | if page.Code != http.StatusOK || !strings.Contains(page.Header().Get("Content-Type"), "text/html") { |
| 27 | t.Fatalf("page: %d %v", page.Code, page.Header()) |
| 28 | } |
| 29 | if page.Header().Get("Cache-Control") != "no-cache" { |
| 30 | t.Fatal("HTML must revalidate to avoid stale asset references") |
| 31 | } |
| 32 | mounted := http.StripPrefix(strings.TrimSuffix(app.prefix, "/"), app.assets) |
| 33 | refs := regexp.MustCompile(`(?:src|href)="([^"]+)"`).FindAllStringSubmatch(page.Body.String(), -1) |
| 34 | if len(refs) < 2 { |
| 35 | t.Fatal("expected a script and stylesheet") |
| 36 | } |
| 37 | for _, ref := range refs { |
| 38 | if !strings.HasPrefix(ref[1], app.prefix) { |
| 39 | t.Fatalf("asset escaped its UI prefix: %s", ref[1]) |
| 40 | } |
| 41 | response := httptest.NewRecorder() |
| 42 | mounted.ServeHTTP(response, httptest.NewRequest(http.MethodGet, ref[1], nil)) |
| 43 | if response.Code != http.StatusOK || response.Body.Len() == 0 { |
| 44 | t.Fatalf("asset %s: status=%d bytes=%d", ref[1], response.Code, response.Body.Len()) |
| 45 | } |
| 46 | if strings.HasSuffix(ref[1], ".css") { |
| 47 | base, err := url.Parse("http://preview" + ref[1]) |
| 48 | if err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | for _, font := range regexp.MustCompile(`url\(([^)]+\.woff2[^)]*)\)`).FindAllStringSubmatch(response.Body.String(), -1) { |
| 52 | fontURL, err := base.Parse(strings.Trim(font[1], "\"'")) |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | got := httptest.NewRecorder() |
| 57 | mounted.ServeHTTP(got, httptest.NewRequest(http.MethodGet, fontURL.String(), nil)) |
| 58 | if got.Code != http.StatusOK || !strings.HasPrefix(got.Body.String(), "wOF2") { |
| 59 | t.Fatalf("CSS font URL %s did not serve WOFF2", fontURL) |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | // All generated static assets remain reachable beneath either mount. |
| 65 | err := fs.WalkDir(output, "dist", func(name string, entry fs.DirEntry, err error) error { |
| 66 | if err != nil { |
| 67 | return err |
| 68 | } |
| 69 | if entry.IsDir() { |
| 70 | return nil |
| 71 | } |
| 72 | name = strings.TrimPrefix(name, "dist/") |
| 73 | if !strings.HasPrefix(name, "assets/") && !strings.HasPrefix(name, "fonts/") { |
| 74 | return nil |
| 75 | } |
| 76 | response := httptest.NewRecorder() |
| 77 | mounted.ServeHTTP(response, httptest.NewRequest(http.MethodGet, app.prefix+name, nil)) |
| 78 | if response.Code != http.StatusOK { |
| 79 | t.Errorf("build asset %s: %d", name, response.Code) |
| 80 | } |
| 81 | return nil |
| 82 | }) |
| 83 | if err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | |
| 87 | head := httptest.NewRecorder() |
| 88 | app.page.ServeHTTP(head, httptest.NewRequest(http.MethodHead, "/", nil)) |
| 89 | if head.Code != http.StatusOK || head.Body.Len() != 0 { |
| 90 | t.Fatal("HEAD should return an empty successful response") |
| 91 | } |
| 92 | }) |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | func TestUIAssetsExposeOnlyStaticBuildFiles(t *testing.T) { |
| 97 | handler := Assets() |
| 98 | for _, name := range []string{"operator/index.html", "client/index.html", ".vite/manifest.json", "assets/../client/index.html", "client/main.ts", "fonts/", "assets/", ""} { |
| 99 | response := httptest.NewRecorder() |
| 100 | handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/"+name, nil)) |
| 101 | if response.Code != http.StatusNotFound { |
| 102 | t.Errorf("unexpected access to %q: %d", name, response.Code) |
| 103 | } |
| 104 | } |
| 105 | } |