Skip to content
quarry Artifact [6cc5cef727]
Login

Artifact [6cc5cef727]

Artifact 6cc5cef72706688adb7508d403b722b06617e101c48fd290a1169d06bd6556c2:


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)
	}
}