package web import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) func get(t *testing.T, h http.Handler, target string, headers map[string]string) (int, string) { t.Helper() r := httptest.NewRequest(http.MethodGet, target, nil) for k, v := range headers { r.Header.Set(k, v) } w := httptest.NewRecorder() h.ServeHTTP(w, r) return w.Code, w.Body.String() } func TestLandingPageTellsYouHowToConnect(t *testing.T) { code, body := get(t, Landing("v1.2.3", true, false, "", ""), "http://llmgw.example/", nil) if code != http.StatusOK { t.Fatalf("status %d", code) } for _, want := range []string{"http://llmgw.example/v1", "Authorization: Bearer", "v1.2.3"} { if !strings.Contains(body, want) { t.Errorf("the page does not mention %q", want) } } } func TestLandingPageSaysNothingAboutWhatIsHere(t *testing.T) { // It is served without a key, to anyone who can reach the address. Anything // on it is public, so nothing about the cluster belongs on it. _, body := get(t, Landing("v1", true, false, "", ""), "http://llmgw.example/", nil) for _, leak := range []string{"llama", "qwen", "10.0.0.", "replica", "node01"} { if strings.Contains(strings.ToLower(body), leak) { t.Errorf("the landing page leaks %q", leak) } } } func TestLandingPageWarnsWhenTheGatewayIsOpen(t *testing.T) { // Someone who reaches this page on an unauthenticated gateway should learn // that from the page rather than by discovering they can use it. _, open := get(t, Landing("v1", false, false, "", ""), "http://llmgw.example/", nil) if !strings.Contains(open, "no API keys configured") { t.Error("an open gateway does not say so") } _, closed := get(t, Landing("v1", true, false, "", ""), "http://llmgw.example/", nil) if strings.Contains(closed, "no API keys configured") { t.Error("a closed gateway claims to be open") } } func TestTheExampleURLSurvivesAReverseProxy(t *testing.T) { // The process sees http and an internal host; the client used https and a // public name. An example naming the internal address is one nobody can // paste anywhere. _, body := get(t, Landing("v1", true, false, "", ""), "http://10.0.0.140:8080/", map[string]string{ "X-Forwarded-Proto": "https", "X-Forwarded-Host": "llmgw.mynodes.xyz", }) if !strings.Contains(body, "https://llmgw.mynodes.xyz/v1") { t.Error("the example does not use the address the client actually used") } if strings.Contains(body, "10.0.0.140") { t.Error("the internal address leaked into the page") } } func TestForwardedHeaderChainsTakeTheFirstEntry(t *testing.T) { // A chain of proxies appends rather than replaces. _, body := get(t, Landing("v1", true, false, "", ""), "http://internal/", map[string]string{ "X-Forwarded-Proto": "https, http", "X-Forwarded-Host": "llmgw.example, internal", }) if !strings.Contains(body, "https://llmgw.example/v1") { t.Errorf("the base URL was built from the wrong entry") } } func TestOnlyTheRootIsTheLandingPage(t *testing.T) { // A catch-all would turn every mistyped path into this page, hiding real // 404s from anyone debugging a client. code, _ := get(t, Landing("v1", true, false, "", ""), "http://x/v1/chat/completionz", nil) if code != http.StatusNotFound { t.Errorf("a mistyped path returned %d", code) } } // fakeSource is a cluster for the dashboard to render. type fakeSource struct{} func (fakeSource) Nodes() []adminapi.NodeView { return []adminapi.NodeView{{ ID: "llmgw-node01", State: "ready", GPUTotal: 8, GPUFree: 4, Classes: []string{"h100-80g"}, Fabrics: []string{"ib-a"}, Replicas: 1, LastSeenAgo: "2s", AgentVersion: "v0.5.0", Unhealthy: []string{"gpu 3: ECC errors"}, }} } func (fakeUsage) UsageByUser(context.Context, time.Time) ([]store.UserUsage, error) { return []store.UserUsage{ {UserID: "usr-1", Email: "ilker@example.com", Keys: 2, Requests: 40, Prompt: 8000, Completion: 4000, CostMicros: 4_500_000, LastSeen: time.Now()}, {UserID: "", Keys: 1, Requests: 3, Prompt: 90, Completion: 40, Errors: 1, LastSeen: time.Now()}, }, nil } func (fakeUsage) UsageByModel(context.Context, time.Time) ([]store.ModelUsage, error) { return []store.ModelUsage{ {Model: "qwen-coder", Requests: 30, Prompt: 6000, Completion: 3000, AvgLatency: 240}, {Model: "qwen", Requests: 13, Prompt: 2090, Completion: 1040, Errors: 1, AvgLatency: 90}, }, nil } func (fakeSource) Summary() adminapi.Summary { return adminapi.Summary{ Nodes: adminapi.NodeTotals{Total: 1, Ready: 1}, GPUs: []adminapi.ClassUse{ {Class: "h100-80g", Total: 8, Used: 3, Unhealthy: 1}, }, Fabrics: []adminapi.FabricUse{ {Fabric: "ib-a", Nodes: 1, LargestFreeBlock: 0, NodesWithWeights: 1, Models: 2}, }, IdleReplicas: []adminapi.IdleReplica{ {ID: "served-abc", Model: "served", IdleSec: 3600, GPUs: 4}, }, } } func (fakeSource) Models() []adminapi.ModelView { return []adminapi.ModelView{ {Name: "served", Ready: 1, Min: 1, Max: 2, Profiles: []string{"tp4"}}, {Name: "stuck", Ready: 0, Min: 1, Max: 1, Profiles: []string{"tp8"}}, } } func (fakeSource) Replicas(string) []adminapi.ReplicaView { return []adminapi.ReplicaView{{ ID: "served-1", Model: "served", State: "ready", Pool: "ib-a/h100-80g", Endpoint: "http://10.0.0.141:20000", AgeSec: 3600, Shards: []adminapi.ShardView{{ID: "served-1-r0", Node: "llmgw-node01"}}, }} } func (fakeSource) Explain(model string) adminapi.ExplainView { if model != "stuck" { return adminapi.ExplainView{Model: model, Placed: true} } return adminapi.ExplainView{Model: "stuck", Rejections: []adminapi.RejectionView{ {Profile: "tp8", Node: "llmgw-node01", Reason: "has 4 free h100-80g GPUs, needs 8"}, }} } type fakeUsage struct { err error buckets []store.Bucket } func (f fakeUsage) UsageBuckets(context.Context, time.Time, time.Duration) ([]store.Bucket, error) { if f.err != nil { return nil, f.err } return f.buckets, nil } func (f fakeUsage) UsageSince(context.Context, time.Time) ([]store.KeyUsage, error) { if f.err != nil { return nil, f.err } return []store.KeyUsage{{ KeyID: "key-1", KeyName: "laptop", KeyPrefix: "sk-llmgw-ab12…", ProjectID: "chat", Requests: 42, Errors: 2, Prompt: 1000, Completion: 3000, CostMicros: 4_500_000, LastAt: time.Now().Add(-time.Minute), }}, nil } func TestDashboardShowsTheCluster(t *testing.T) { h := Dashboard(fakeSource{}, fakeUsage{}, "v1", time.Second, true, NavExtras{}, nil) code, body := get(t, h, "http://127.0.0.1:9090/", nil) if code != http.StatusOK { t.Fatalf("status %d", code) } for _, want := range []string{ "llmgw-node01", "h100-80g", "served-1", "http://10.0.0.141:20000", "ECC errors", "laptop", "4.50", // spend shown in units, not micro-units } { if !strings.Contains(body, want) { t.Errorf("the dashboard does not show %q", want) } } } func TestDashboardExplainsOnlyWhatIsUnplaced(t *testing.T) { // Asking why a satisfied model was not placed gives an empty answer, and a // page full of empty answers hides the one that matters. h := Dashboard(fakeSource{}, nil, "v1", time.Second, true, NavExtras{}, nil) _, body := get(t, h, "http://127.0.0.1:9090/", nil) if !strings.Contains(body, "needs 8") { t.Error("the unplaced model has no explanation") } if strings.Count(body, "Why nothing was placed") != 1 { t.Error("the explanation section is missing or duplicated") } } func TestDashboardSurvivesAFailingUsageQuery(t *testing.T) { // A reporting query failing must not take the page with it: the cluster // state is what someone is most likely here for. h := Dashboard(fakeSource{}, fakeUsage{err: context.DeadlineExceeded}, "v1", time.Second, true, NavExtras{}, nil) code, body := get(t, h, "http://127.0.0.1:9090/", nil) if code != http.StatusOK { t.Fatalf("status %d", code) } if !strings.Contains(body, "llmgw-node01") { t.Error("the cluster state was lost with the usage query") } } func TestDashboardSaysWhenThereIsNothingToAccount(t *testing.T) { h := Dashboard(fakeSource{}, nil, "v1", time.Second, true, NavExtras{}, nil) _, body := get(t, h, "http://127.0.0.1:9090/", nil) if !strings.Contains(body, "Authentication is off") { t.Error("the page does not explain the empty usage section") } } func TestDashboardRendersAnEmptyCluster(t *testing.T) { // The state a fresh install is in, and the one most likely to be looked at // with a template that has never seen it. h := Dashboard(emptySource{}, nil, "v1", time.Second, true, NavExtras{}, nil) code, body := get(t, h, "http://127.0.0.1:9090/", nil) if code != http.StatusOK { t.Fatalf("status %d", code) } if !strings.Contains(body, "No agents have registered") { t.Error("an empty cluster renders nothing useful") } } type emptySource struct{} func (emptySource) Nodes() []adminapi.NodeView { return nil } func (emptySource) Models() []adminapi.ModelView { return nil } func (emptySource) Replicas(string) []adminapi.ReplicaView { return nil } func (emptySource) Explain(string) adminapi.ExplainView { return adminapi.ExplainView{} } func (emptySource) Summary() adminapi.Summary { return adminapi.Summary{} } func TestStaticServesAssetsAndNothingElse(t *testing.T) { // The assets live in their own embed. A file server over the tree that also // carries the templates would serve those as well, and a directory listing // would publish the shape of the tree to nobody's benefit. h := Static() for _, path := range []string{ "/_static/base.css", "/_static/katex/katex.min.css", "/_static/katex/katex.min.js", "/_static/katex/auto-render.min.js", "/_static/katex/fonts/KaTeX_Main-Regular.woff2", } { if code, _ := get(t, h, "http://x"+path, nil); code != http.StatusOK { t.Errorf("%s is not served: %d", path, code) } } for _, path := range []string{ "/_static/dashboard.html", "/_static/chat.html", "/_static/", "/_static/katex/", "/_static/katex/fonts/", "/_static/../web.go", } { if code, _ := get(t, h, "http://x"+path, nil); code == http.StatusOK { t.Errorf("%s was served", path) } } } func TestTheLandingPageSaysWhereToGetAKey(t *testing.T) { // A page that explains the API and leaves the reader with no route to using // it is where onboarding stops. _, body := get(t, Landing("v1", true, false, "platform@example.com", ""), "http://x/", nil) if !strings.Contains(body, `href="mailto:platform@example.com"`) { t.Error("an address was not turned into a mailto link") } // With signup closed, the route is a person; the page still has to say what // arriving looks like. if !strings.Contains(body, "shown once") { t.Error("the page does not say a key cannot be recovered") } } func TestContactRenderingByKind(t *testing.T) { cases := []struct{ in, text, href string }{ {"platform@example.com", "platform@example.com", "mailto:platform@example.com"}, {"mailto:ops@example.com", "mailto:ops@example.com", "mailto:ops@example.com"}, {"https://wiki.example/llmgw", "https://wiki.example/llmgw", "https://wiki.example/llmgw"}, {"http://wiki.example", "http://wiki.example", "http://wiki.example"}, // Not addresses and not URLs: shown, not linked. {"#platform on Slack", "#platform on Slack", ""}, {"Ayse", "Ayse", ""}, {"", "", ""}, {" ", "", ""}, } for _, c := range cases { text, href := contactLink(c.in) if text != c.text || href != c.href { t.Errorf("contactLink(%q) = (%q, %q), want (%q, %q)", c.in, text, href, c.text, c.href) } } } func TestADangerousContactSchemeIsNeverLinked(t *testing.T) { // This string comes from configuration, and configuration is not always // written by whoever runs the gateway. A javascript: href on a page served // to every visitor is a stored XSS with a very short path to it; escaping // the text does not make the scheme safe. for _, bad := range []string{ "javascript:alert(1)", "JavaScript:alert(1)", "data:text/html,", "vbscript:msgbox(1)", "file:///etc/passwd", } { text, href := contactLink(bad) if href != "" { t.Errorf("contactLink(%q) produced href %q", bad, href) } if text == "" { t.Errorf("contactLink(%q) dropped the text entirely", bad) } _, body := get(t, Landing("v1", true, false, bad, ""), "http://x/", nil) if strings.Contains(body, "href=\"javascript") || strings.Contains(body, "href=\"data:") { t.Errorf("%q reached the page as a link", bad) } } } func TestTheNoticeIsEscaped(t *testing.T) { _, body := get(t, Landing("v1", true, false, "", ""), "http://x/", nil) if strings.Contains(body, "") { t.Error("the notice was rendered as markup") } if !strings.Contains(body, "<script>") { t.Error("the notice was dropped rather than escaped") } } func TestWithoutAContactThePageStillSaysSomethingUseful(t *testing.T) { _, body := get(t, Landing("v1", true, false, "", ""), "http://x/", nil) if !strings.Contains(body, "whoever operates this gateway") { t.Error("an unconfigured gateway leaves the reader with nothing") } } func TestWithSignupOpenTheLandingPagePointsAtIt(t *testing.T) { // The route from "I found this address" to "I have a key" has to be on the // page, or the reader is stuck whatever else it says. _, body := get(t, Landing("v1", true, true, "", ""), "http://x/", nil) for _, want := range []string{`href="/signup"`, `href="/login"`, `href="/profile"`} { if !strings.Contains(body, want) { t.Errorf("the page does not link to %s", want) } } } func TestTheDashboardDrawsTrafficWhenThereIsSome(t *testing.T) { now := time.Now().Truncate(time.Hour) usage := fakeUsage{buckets: []store.Bucket{ {Start: now.Add(-2 * time.Hour), Requests: 40, Prompt: 4000, Completion: 900, LatencyMs: 120}, {Start: now.Add(-time.Hour)}, {Start: now, Requests: 95, Prompt: 9000, Completion: 2100, LatencyMs: 310}, }} h := Dashboard(fakeSource{}, usage, "v1", time.Second, true, NavExtras{}, nil) _, body := get(t, h, "http://127.0.0.1:9090/", nil) if !strings.Contains(body, "4<") { t.Errorf("free count is wrong or missing:\n%s", page) } } func TestAUsageMeterCannotOverrunItsColumn(t *testing.T) { // The two counts it divides can disagree for an instant — a GPU freed // between one read and the next — and a bar wider than its cell wrecks the // table. pct, ok := funcs["pct"].(func(int, int) int) if !ok { t.Fatal("the pct helper is missing") } for _, tc := range [][2]int{{0, 0}, {9, 8}, {-1, 8}, {100, 8}} { if got := pct(tc[0], tc[1]); got < 0 || got > 100 { t.Errorf("pct(%d,%d) = %d", tc[0], tc[1], got) } } } func TestTheDashboardShowsWhoIsUsingTheCluster(t *testing.T) { // Per key is the right unit for revoking something and the wrong one for // answering who is using this cluster: one person holds a laptop key, a CI // key and a notebook key, and adding three rows up by eye gets skipped. h := Dashboard(fakeSource{}, fakeUsage{}, "v1", time.Second, true, NavExtras{}, nil) code, page := get(t, h, "http://127.0.0.1:9090/", nil) if code != http.StatusOK { t.Fatalf("status %d", code) } for _, want := range []string{"People", "ilker@example.com", "12.0k"} { if !strings.Contains(page, want) { t.Errorf("the dashboard does not show %q", want) } } // A key with no owner predates accounts. Calling it "unknown" would imply // something is wrong; it is an operator-issued key doing its job. if !strings.Contains(page, "operator-issued keys") { t.Error("an unowned key is not explained") } } func TestTheDashboardShowsWhatIsActuallyRunning(t *testing.T) { h := Dashboard(fakeSource{}, fakeUsage{}, "v1", time.Second, true, NavExtras{}, nil) _, page := get(t, h, "http://127.0.0.1:9090/", nil) for _, want := range []string{"What is being run", "qwen-coder", "240 ms"} { if !strings.Contains(page, want) { t.Errorf("the dashboard does not show %q", want) } } } func TestAReportingFailureDoesNotTakeThePageWithIt(t *testing.T) { // The cluster state is what someone is most likely here for, and a usage // query is the part most likely to be slow or broken. h := Dashboard(fakeSource{}, brokenUsage{}, "v1", time.Second, true, NavExtras{}, nil) code, page := get(t, h, "http://127.0.0.1:9090/", nil) if code != http.StatusOK { t.Fatalf("status %d; a reporting failure took the page down", code) } if !strings.Contains(page, "llmgw-node01") { t.Error("the cluster state was lost along with the usage query") } } // brokenUsage fails every query, as a database under pressure would. type brokenUsage struct{} func (brokenUsage) UsageSince(context.Context, time.Time) ([]store.KeyUsage, error) { return nil, errUsage } func (brokenUsage) UsageBuckets(context.Context, time.Time, time.Duration) ([]store.Bucket, error) { return nil, errUsage } func (brokenUsage) UsageByUser(context.Context, time.Time) ([]store.UserUsage, error) { return nil, errUsage } func (brokenUsage) UsageByModel(context.Context, time.Time) ([]store.ModelUsage, error) { return nil, errUsage } var errUsage = errors.New("the database is unhappy") func TestEveryClientExampleIsInThePageItself(t *testing.T) { // The tabs are radio buttons and a stylesheet, nothing more. If a panel were // fetched when its tab was picked, a reader with no JavaScript would choose // a tab and get an empty box — and this is the page people arrive on before // they have any reason to trust the site enough to enable anything. _, body := get(t, Landing("v1", true, false, "", ""), "http://llmgw.example/", nil) for _, want := range []string{ "curl http://llmgw.example/v1/chat/completions", `base_url="http://llmgw.example/v1"`, `"baseURL": "http://llmgw.example/v1"`, } { if !strings.Contains(body, want) { t.Errorf("the page does not carry %q", want) } } } func TestAHostileForwardedHostCannotWriteMarkup(t *testing.T) { // Nothing overwrites X-Forwarded-Host when the gateway is reached directly, // so its value is whatever the caller sent, and it is now reflected into // several places on the page rather than one. Escaping is the template's // job; this is what says so out loud, and what would fail if any of those // places ever stopped being one. _, body := get(t, Landing("v1", true, false, "", ""), "http://internal/", map[string]string{ "X-Forwarded-Host": `evil">alert(1)") { t.Error("a forwarded host reached the page as markup") } if !strings.Contains(body, "evil") { t.Error("the host was dropped rather than escaped, so the test proves nothing") } } func TestTheLandingPageDiagramDescribesItself(t *testing.T) { // It carries the one idea the prose does not: that fabrics are separate. A // reader using a screen reader gets that from the label or not at all. _, body := get(t, Landing("v1", true, false, "", ""), "http://x/", nil) if !strings.Contains(body, `role="img"`) || !strings.Contains(body, "aria-label=") { t.Error("the schematic is decoration to anyone not looking at it") } }