Artifact 6cc5cef72706688adb7508d403b722b06617e101c48fd290a1169d06bd6556c2:
- File
internal/web/opstrend_test.go
— part of check-in
[2d033f106e]
at
2026-08-26 13:37:10
on branch ops-paths
— ops: the path actually requested, and two broken links
Recent renders showed the route pattern, so a dozen rows all read GET /r/{repo}/timeline and none could say which repository was slow. The URL is now recorded and shown, with the route beside it as the link to its drilldown.
Two bugs found by clicking through rather than by testing the parts:
queryesc double-encoded every URL it touched, because html/template already escapes a value in query context. The routes panel linked to its own drilldown with %252F for a slash, so every link 500'd. Six call sites, all wrong; the function is gone.
The days control I added with the trend forwarded an empty parameter, and a numeric field with a default takes it when the key is MISSING and refuses when it is present and empty.
Routes panel gains a filter and a sort; renders gain a deeper-history page reading the store rather than the ring. (user: dmestas, size: 4883) [annotate] [blame] [check-ins using] [more...]
package web import ( "bytes" "net/url" "strings" "testing" ) func renderRoute(t *testing.T, v opsRouteView, store bool) string { t.Helper() var ops opsView ops.BudgetMs = 500 ops.Store.Configured = store var buf bytes.Buffer if err := opsRoutePage.ExecuteTemplate(&buf, "content", pageData{Title: "route", OpsRoute: v, Ops: ops}); err != nil { t.Fatal(err) } return buf.String() } func trendPoint(day string, n int, p50, p95, max int64) struct { Day string `json:"day"` N int `json:"n"` P50Ms int64 `json:"p50Ms"` P95Ms int64 `json:"p95Ms"` MaxMs int64 `json:"maxMs"` } { var p struct { Day string `json:"day"` N int `json:"n"` P50Ms int64 `json:"p50Ms"` P95Ms int64 `json:"p95Ms"` MaxMs int64 `json:"maxMs"` } p.Day, p.N, p.P50Ms, p.P95Ms, p.MaxMs = day, n, p50, p95, max return p } // The question after a deploy is not "how fast is this now" but "is it // worse than it was", and a snapshot of one process's memory cannot // answer it. func TestRoutePageShowsTheTrend(t *testing.T) { var v opsRouteView v.Path = "GET /{$}" v.TrendDays = 7 v.Trend = append(v.Trend, trendPoint("2026-08-24", 20, 19, 28, 29)) v.Trend = append(v.Trend, trendPoint("2026-08-26", 20, 190, 280, 290)) body := renderRoute(t, v, true) for _, want := range []string{"Across days", "2026-08-24", "28ms", "2026-08-26", "280ms"} { if !strings.Contains(body, want) { t.Errorf("the trend table is missing %q:\n%s", want, body) } } } // A day over budget is marked, the same way the panel marks one. func TestRouteTrendMarksDaysOverBudget(t *testing.T) { var v opsRouteView v.Path = "GET /{$}" v.Trend = append(v.Trend, trendPoint("2026-08-26", 20, 100, 900, 1200)) if !strings.Contains(renderRoute(t, v, true), "ci-fail-text") { t.Error("a 900ms p95 against a 500ms budget renders unmarked") } var ok opsRouteView ok.Path = "GET /{$}" ok.Trend = append(ok.Trend, trendPoint("2026-08-26", 20, 10, 20, 30)) if strings.Contains(renderRoute(t, ok, true), "ci-fail-text") { t.Error("a route comfortably inside budget renders as failing") } } // No history and history switched off are different answers, and a page // that gives one for the other sends an operator looking for traffic // that was never recorded. func TestRoutePageSaysWhyTheTrendIsEmpty(t *testing.T) { var v opsRouteView v.Path = "GET /{$}" v.TrendDays = 14 on := renderRoute(t, v, true) if !strings.Contains(on, "No history for this route in the last 14 days") { t.Errorf("a configured store with no rows does not say so:\n%s", on) } off := renderRoute(t, v, false) if !strings.Contains(off, "History is off") { t.Errorf("an unconfigured store is reported as no traffic:\n%s", off) } if strings.Contains(off, "No history for this route") { t.Error("history-off is reported as an empty window") } } // An absent query parameter and an empty one are different things to // the validator: a numeric field with a default takes it when the key // is MISSING and refuses when the key is present and empty. Forwarding // Get() unconditionally turns every absent optional into a 400 — which // is how the drilldown started answering 500 to its own links the // moment it gained a days control. func TestPresentOmitsWhatWasNotSent(t *testing.T) { q := url.Values{"path": {"GET /{$}"}, "days": {""}} got := present(q, "path", "days", "sort") if got.Get("path") != "GET /{$}" { t.Errorf("path=%q", got.Get("path")) } if _, ok := got["days"]; ok { t.Error("an empty days was forwarded; the validator refuses it") } if _, ok := got["sort"]; ok { t.Error("a key that was never sent was invented") } } // html/template escapes a value in URL query context already, so // escaping it first produces %252F for a slash. The ops page linked to // its own drilldown with a double-encoded route, and the drilldown // answered 500 to every link the panel above it drew. func TestDrilldownLinksAreEncodedExactlyOnce(t *testing.T) { var v opsView v.BudgetMs = 500 var p struct { Path string `json:"path"` Count int `json:"count"` P50Ms int64 `json:"p50Ms"` P95Ms int64 `json:"p95Ms"` MaxMs int64 `json:"maxMs"` } p.Path, p.Count = "GET /r/{repo}/timeline", 3 v.Paths = append(v.Paths, p) body := renderOps(t, v) i := strings.Index(body, "/fleet/ops/route?path=") if i < 0 { t.Fatalf("no drilldown link rendered:\n%s", body) } href := body[i+len("/fleet/ops/route?path="):] href = href[:strings.IndexAny(href, `"`)] if strings.Contains(href, "%25") { t.Errorf("double-encoded link %q — %%25 is an escaped escape", href) } // It must survive the round trip a browser and the mux perform. got, err := url.QueryUnescape(href) if err != nil { t.Fatalf("link %q does not decode: %v", href, err) } if got != "GET /r/{repo}/timeline" { t.Errorf("link decodes to %q, want the route it names", got) } }