package testsupport import ( "bufio" "io" "net/http" "strings" "testing" "time" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config" ) // startForTest binds the stub on an ephemeral port so the suite does not fight // with anything already holding 9080, and fails fast if Start blocks — the // original harness called ListenAndServe inline and never returned. func startForTest(t *testing.T) *Snapshot { t.Helper() t.Setenv("HARNESS_STUB_PORT", "0") type result struct { snap *Snapshot err error } done := make(chan result, 1) go func() { snap, err := Start() done <- result{snap, err} }() select { case r := <-done: if r.err != nil { t.Fatalf("Start() error: %v", r.err) } t.Cleanup(Close) return r.snap case <-time.After(5 * time.Second): t.Fatal("Start() did not return within 5s — it is blocking instead of serving in the background") return nil } } func TestStartServesFixedJSON(t *testing.T) { snap := startForTest(t) resp, err := snap.Client.Get(snap.URL(PathJSON)) if err != nil { t.Fatalf("GET %s: %v", PathJSON, err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } if got := resp.Header.Get("Content-Type"); got != "application/json" { t.Errorf("Content-Type = %q, want %q", got, "application/json") } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("read body: %v", err) } if want := `{"status":"ok"}`; strings.TrimSpace(string(body)) != want { t.Errorf("body = %q, want %q", strings.TrimSpace(string(body)), want) } } func TestSSEArrivesIncrementally(t *testing.T) { snap := startForTest(t) resp, err := snap.Client.Get(snap.URL(PathSSE)) if err != nil { t.Fatalf("GET %s: %v", PathSSE, err) } defer func() { _ = resp.Body.Close() }() // The first event must be readable before the handler has written the last // one. If the response were buffered to completion, this read would only // unblock after every token plus every gap had elapsed. deadline := time.Now().Add(time.Duration(len(SSETokens)) * SSEGap) reader := bufio.NewReader(resp.Body) line, err := reader.ReadString('\n') if err != nil { t.Fatalf("read first event: %v", err) } if time.Now().After(deadline) { t.Error("first SSE event arrived only after the whole stream was written; not incremental") } if want := "data: " + SSETokens[0]; strings.TrimSpace(line) != want { t.Errorf("first event = %q, want %q", strings.TrimSpace(line), want) } rest, err := io.ReadAll(reader) if err != nil { t.Fatalf("read remaining events: %v", err) } if !strings.Contains(string(rest), "data: [DONE]") { t.Error("stream did not end with the [DONE] sentinel") } } func TestStartIsIdempotent(t *testing.T) { first := startForTest(t) second, err := Start() if err != nil { t.Fatalf("second Start() error: %v", err) } if first.BaseURL != second.BaseURL { t.Errorf("second Start() bound a different address: %q vs %q", second.BaseURL, first.BaseURL) } } func TestHarnessFixtureLoadsAndStaysOnLoopback(t *testing.T) { // DefaultConfigPath is relative to the repo root; tests run in the package // directory, so walk back up to it. routes, err := config.LoadRoutesFromFile("../../" + DefaultConfigPath) if err != nil { t.Fatalf("LoadRoutesFromFile(%s): %v", DefaultConfigPath, err) } if len(routes) == 0 { t.Fatal("fixture declared no routes") } for name, route := range routes { if !strings.HasPrefix(route.Upstream.Address, "127.0.0.1:") { t.Errorf("route %q upstream %q is not on loopback", name, route.Upstream.Address) } if route.Upstream.AuthRequired { t.Errorf("route %q requires auth; the harness must run with no credentials", name) } } }