package websrv import ( "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestNewHandlerRejectsMissingWebDir(t *testing.T) { t.Parallel() _, err := NewHandler(filepath.Join(t.TempDir(), "missing"), "http://127.0.0.1:8080") if err == nil { t.Fatal("expected error for missing webdir") } } func TestNewHandlerRejectsInvalidAPIURL(t *testing.T) { t.Parallel() webDir := t.TempDir() _, err := NewHandler(webDir, "not-a-url") if err == nil { t.Fatal("expected error for invalid API_URL") } } func TestSPAServesIndexAndAssets(t *testing.T) { t.Parallel() webDir := t.TempDir() mustWriteFile(t, filepath.Join(webDir, "index.html"), "home") mustWriteFile(t, filepath.Join(webDir, "assets", "app.js"), "console.log(1)") api := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNoContent) })) t.Cleanup(api.Close) handler, err := NewHandler(webDir, api.URL) if err != nil { t.Fatalf("NewHandler: %v", err) } indexRecorder := httptest.NewRecorder() handler.ServeHTTP(indexRecorder, httptest.NewRequest(http.MethodGet, "/", nil)) if indexRecorder.Code != http.StatusOK { t.Fatalf("GET / status = %d", indexRecorder.Code) } if !strings.Contains(indexRecorder.Body.String(), "home") { t.Fatalf("GET / body = %q", indexRecorder.Body.String()) } assetRecorder := httptest.NewRecorder() handler.ServeHTTP(assetRecorder, httptest.NewRequest(http.MethodGet, "/assets/app.js", nil)) if assetRecorder.Code != http.StatusOK { t.Fatalf("GET /assets/app.js status = %d", assetRecorder.Code) } if assetRecorder.Body.String() != "console.log(1)" { t.Fatalf("asset body = %q", assetRecorder.Body.String()) } spaRecorder := httptest.NewRecorder() handler.ServeHTTP(spaRecorder, httptest.NewRequest(http.MethodGet, "/settings", nil)) if spaRecorder.Code != http.StatusOK { t.Fatalf("GET /settings status = %d", spaRecorder.Code) } if !strings.Contains(spaRecorder.Body.String(), "home") { t.Fatalf("SPA fallback body = %q", spaRecorder.Body.String()) } } func TestProxyForwardsAPIAndHealth(t *testing.T) { t.Parallel() webDir := t.TempDir() mustWriteFile(t, filepath.Join(webDir, "index.html"), "home") var seenPaths []string var seenForwardedHost string api := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { seenPaths = append(seenPaths, request.URL.Path) seenForwardedHost = request.Header.Get("X-Forwarded-Host") writer.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(writer, `{"ok":true}`) })) t.Cleanup(api.Close) handler, err := NewHandler(webDir, api.URL) if err != nil { t.Fatalf("NewHandler: %v", err) } healthRecorder := httptest.NewRecorder() handler.ServeHTTP(healthRecorder, httptest.NewRequest(http.MethodGet, "/health", nil)) if healthRecorder.Code != http.StatusOK { t.Fatalf("GET /health status = %d", healthRecorder.Code) } apiRequest := httptest.NewRequest(http.MethodGet, "/api/v1/status", nil) apiRequest.Host = "admin.example.test:5173" apiRecorder := httptest.NewRecorder() handler.ServeHTTP(apiRecorder, apiRequest) if apiRecorder.Code != http.StatusOK { t.Fatalf("GET /api/v1/status status = %d", apiRecorder.Code) } if len(seenPaths) != 2 || seenPaths[0] != "/health" || seenPaths[1] != "/api/v1/status" { t.Fatalf("proxied paths = %#v", seenPaths) } if seenForwardedHost != "admin.example.test:5173" { t.Fatalf("X-Forwarded-Host = %q, want browser host", seenForwardedHost) } } func mustWriteFile(t *testing.T, path string, contents string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("mkdir: %v", err) } if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { t.Fatalf("write: %v", err) } }