package webui import ( "io" "net/http" "net/http/httptest" "strings" "testing" "testing/fstest" ) const ( indexBody = "index" apiKeysBody = "api-keys" cssBody = "body{color:red}" immutableCC = "public, max-age=31536000, immutable" noCacheCC = "no-cache" contentTypeH = "Content-Type" cacheCtrlH = "Cache-Control" ) func newTestFS() fstest.MapFS { return fstest.MapFS{ "index.html": {Data: []byte(indexBody)}, "api-keys/index.html": {Data: []byte(apiKeysBody)}, "_next/static/css/app.abcdef.css": {Data: []byte(cssBody)}, } } func doGet(t *testing.T, h http.Handler, path string) *http.Response { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec.Result() } func readBody(t *testing.T, resp *http.Response) string { t.Helper() b, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("read body: %v", err) } _ = resp.Body.Close() return string(b) } func TestHandler_RootServesIndex(t *testing.T) { resp := doGet(t, handlerForFS(newTestFS()), "/") if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } if ct := resp.Header.Get(contentTypeH); !strings.HasPrefix(ct, "text/html") { t.Errorf("Content-Type = %q, want text/html*", ct) } if got := readBody(t, resp); got != indexBody { t.Errorf("body = %q, want %q", got, indexBody) } } // TestHandler_IndexHtmlDoesNotRedirect guards against a regression where // the handler delegated to http.FileServer, which 301-redirects any URL // ending in /index.html to ./ and produced infinite loops on every UI // route that resolved to a directory index. func TestHandler_IndexHtmlDoesNotRedirect(t *testing.T) { resp := doGet(t, handlerForFS(newTestFS()), "/index.html") if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200 (no redirect)", resp.StatusCode) } if loc := resp.Header.Get("Location"); loc != "" { t.Errorf("Location = %q, want empty (no redirect)", loc) } if got := readBody(t, resp); got != indexBody { t.Errorf("body = %q, want %q", got, indexBody) } } func TestHandler_RouteResolvesToDirectoryIndex(t *testing.T) { resp := doGet(t, handlerForFS(newTestFS()), "/api-keys") if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } if got := readBody(t, resp); got != apiKeysBody { t.Errorf("body = %q, want %q", got, apiKeysBody) } if cc := resp.Header.Get(cacheCtrlH); cc != noCacheCC { t.Errorf("Cache-Control = %q, want %q", cc, noCacheCC) } } func TestHandler_NextStaticAssetCacheHeaders(t *testing.T) { resp := doGet(t, handlerForFS(newTestFS()), "/_next/static/css/app.abcdef.css") if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } if cc := resp.Header.Get(cacheCtrlH); cc != immutableCC { t.Errorf("Cache-Control = %q, want %q", cc, immutableCC) } if ct := resp.Header.Get(contentTypeH); !strings.HasPrefix(ct, "text/css") { t.Errorf("Content-Type = %q, want text/css*", ct) } if got := readBody(t, resp); got != cssBody { t.Errorf("body = %q, want %q", got, cssBody) } } func TestHandler_SPAFallbackForUnknownPath(t *testing.T) { resp := doGet(t, handlerForFS(newTestFS()), "/this/route/does/not/exist") if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200 (SPA fallback)", resp.StatusCode) } if got := readBody(t, resp); got != indexBody { t.Errorf("body = %q, want SPA index %q", got, indexBody) } } func TestHandler_PlaceholderWhenNoIndex(t *testing.T) { empty := fstest.MapFS{} resp := doGet(t, handlerForFS(empty), "/") if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } body := readBody(t, resp) if !strings.Contains(body, "OrchestrAD") { t.Errorf("placeholder body missing 'OrchestrAD': %q", body) } } // TestHandler_LegacyAuthPathRedirectsToLogin locks in the 301 redirect from // pre-flatten Spike template paths to /login, so stale bookmarks can never // feed a junk URL back into the SPA router. func TestHandler_LegacyAuthPathRedirectsToLogin(t *testing.T) { cases := []string{ "/auth/auth1/login", "/auth/auth1/login/", "/auth/auth1", "/auth/auth2/register", } for _, p := range cases { p := p t.Run(p, func(t *testing.T) { resp := doGet(t, handlerForFS(newTestFS()), p) if resp.StatusCode != http.StatusMovedPermanently { t.Fatalf("status = %d, want 301 for %s", resp.StatusCode, p) } if loc := resp.Header.Get("Location"); loc != "/login" { t.Errorf("Location = %q, want /login for %s", loc, p) } }) } } func TestHandler_HeadRequestOmitsBody(t *testing.T) { req := httptest.NewRequest(http.MethodHead, "/", nil) rec := httptest.NewRecorder() handlerForFS(newTestFS()).ServeHTTP(rec, req) resp := rec.Result() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } if cl := resp.Header.Get("Content-Length"); cl == "" || cl == "0" { t.Errorf("Content-Length = %q, want non-zero size", cl) } if got := readBody(t, resp); got != "" { t.Errorf("HEAD body = %q, want empty", got) } }