package web
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"html/template"
"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, "