package web import ( "context" "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), "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), "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), "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), "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), "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), "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), "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 (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 } 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, 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, 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, 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, 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, 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 TestStaticIsNotAFileServer(t *testing.T) { // A file server over the embedded templates would also serve the templates, // which is harmless today and the kind of thing that stops being harmless. h := Static() if code, _ := get(t, h, "http://x/_static/base.css", nil); code != http.StatusOK { t.Errorf("the stylesheet is not served: %d", code) } for _, path := range []string{ "/_static/dashboard.html", "/_static/", "/_static/../web.go", } { if code, _ := get(t, h, "http://x"+path, nil); code == http.StatusOK { t.Errorf("%s was served", path) } } }