package web
import (
"context"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/mcpclient/mcptest"
"gitea.mynodes.xyz/ilker/llmgw/internal/secrets"
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
)
// mcpPage mounts /admin/mcp over a store of its own, for a person.
func mcpPage(t *testing.T, user store.User) (*http.ServeMux, *store.DB, *auditSink) {
t.Helper()
st, err := store.OpenSQLite(context.Background(), filepath.Join(t.TempDir(), "llmgw.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
secrets.SetStore(st.StoredSecret)
t.Cleanup(func() { secrets.SetStore(nil) })
p := NewAccountPages(fakeAccounts{user: user}, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil)))
sink := &auditSink{}
p.SetAudit(sink)
mux := http.NewServeMux()
p.RegisterAdminMCP(mux, st, nil)
return mux, st, sink
}
var mcpAdmin = store.User{ID: "a", Email: "a@example.com", IsAdmin: true}
func TestTheMCPPageIsForAdministratorsOnly(t *testing.T) {
for _, role := range []string{store.RoleViewer, store.RoleOperator, store.RoleBilling} {
mux, _, _ := mcpPage(t, store.User{ID: "s", Email: "s@example.com", Role: role})
if w := mirrorGet(t, mux, "/admin/mcp"); w.Code != http.StatusNotFound {
t.Errorf("a %s got %d", role, w.Code)
}
}
mux, _, _ := mcpPage(t, mcpAdmin)
body := mirrorGet(t, mux, "/admin/mcp").Body.String()
if !strings.Contains(body, `href="/admin/mcp" aria-current="page"`) || !strings.Contains(body, "/mcp --header") {
t.Errorf("the menu should mark the page, and the page say how an agent is pointed at it: %s", body)
}
if w := mirrorPost(t, mux, "/admin/mcp/add", url.Values{"name": {"x"}, "url": {"https://x/mcp"}}); w.Code != http.StatusBadRequest {
t.Errorf("a form without its token got %d", w.Code)
}
}
func TestAnMCPServerIsAddedTestedAndItsCredentialKeptSealed(t *testing.T) {
mux, st, sink := mcpPage(t, mcpAdmin)
up := mcptest.New()
defer up.Close()
// What the server is sent, every request: the fake records calls only.
var auth []string
target, _ := url.Parse(up.URL)
front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth = append(auth, r.Header.Get("Authorization"))
httputil.NewSingleHostReverseProxy(target).ServeHTTP(w, r)
}))
defer front.Close()
w := mirrorPost(t, mux, "/admin/mcp/add", url.Values{"csrf": {"tok"}, "name": {"jira"}, "url": {front.URL},
"description": {"tickets"}, "auth": {"bearer"}, "secret_value": {"s3cret-token"}, "rate": {"30"}})
body := w.Body.String()
if !strings.Contains(body, "Added jira") || strings.Contains(body, "s3cret-token") {
t.Fatalf("not added, or the token shown: %s", body)
}
m, err := st.MCPServer(context.Background(), "jira")
if err != nil || m.HeaderRefs["Authorization"] != "store:mcp/jira/Authorization" || m.RatePerMinute != 30 || !m.Enabled {
t.Fatalf("stored %+v, %v", m, err)
}
for _, e := range sink.events {
if strings.Contains(e.Detail, "s3cret-token") || strings.Contains(e.Target, "s3cret-token") {
t.Errorf("the audit line holds the token: %+v", e)
}
}
if !strings.Contains(body, "nobody yet") || !strings.Contains(body, "sealed") {
t.Errorf("a new server is granted to nobody, its credential sealed: %s", body)
}
// The test asks it for its tools with the sealed token.
body = mirrorPost(t, mux, "/admin/mcp/jira/test", url.Values{"csrf": {"tok"}}).Body.String()
if !strings.Contains(body, "jira answers, with 3 tools") || !strings.Contains(body, "jira.echo") {
t.Fatalf("the test's answer: %s", body)
}
if len(auth) == 0 || auth[len(auth)-1] != "Bearer s3cret-token" {
t.Error("the server was not sent its token")
}
// Off, then removed.
mirrorPost(t, mux, "/admin/mcp/jira/disable", url.Values{"csrf": {"tok"}})
if m, _ := st.MCPServer(context.Background(), "jira"); m.Enabled {
t.Error("still on")
}
mirrorPost(t, mux, "/admin/mcp/jira/remove", url.Values{"csrf": {"tok"}})
if _, err := st.MCPServer(context.Background(), "jira"); err == nil {
t.Error("still there")
}
}
func TestAnMCPServersToolsAreGrantedBesideAnotherServers(t *testing.T) {
mux, st, _ := mcpPage(t, mcpAdmin)
ctx := context.Background()
org, _ := st.CreateOrg(ctx, "Acme")
team, _ := st.CreateTeam(ctx, org, "ml")
rpm := 60
if err := st.PutLimits(ctx, store.Limits{Scope: store.ScopeTeam, ScopeID: team, RPM: &rpm, AllowedTools: []string{"jira.*"}}); err != nil {
t.Fatal(err)
}
if err := st.PutMCPServer(ctx, store.MCPServer{Name: "hr", URL: "https://hr/mcp", Enabled: true}); err != nil {
t.Fatal(err)
}
limits := func() store.Limits {
l, _ := st.LimitsFor(ctx, store.ScopeTeam, team)
return l
}
body := mirrorPost(t, mux, "/admin/mcp/hr/grant", url.Values{"csrf": {"tok"}, "scope": {"team"}, "which": {"Acme/ml"},
"tools": {"hr.search, hr.get"}}).Body.String()
l := limits()
if !strings.Contains(body, "hr's tools granted to Acme/ml") ||
!slices.Equal(l.AllowedTools, []string{"jira.*", "hr.search", "hr.get"}) || l.RPM == nil || *l.RPM != 60 {
t.Fatalf("the team's limits are %+v; jira's grant and its rate should have stayed: %s", l, body)
}
if !strings.Contains(body, "team Acme/ml: hr.search, hr.get") {
t.Errorf("the grant should show on hr's row: %s", body)
}
// Another server's tools, or no such team, are refused.
if body := mirrorPost(t, mux, "/admin/mcp/hr/grant", url.Values{"csrf": {"tok"}, "scope": {"team"}, "which": {"Acme/ml"},
"tools": {"jira.delete"}}).Body.String(); !strings.Contains(body, "jira.delete is not one of hr's tools") {
t.Errorf("another server's tool was granted: %s", body)
}
if body := mirrorPost(t, mux, "/admin/mcp/hr/grant", url.Values{"csrf": {"tok"}, "scope": {"team"}, "which": {"Acme/none"},
"tools": {"hr.*"}}).Body.String(); !strings.Contains(body, "There is no Acme/none") {
t.Errorf("a team that does not exist was granted: %s", body)
}
mirrorPost(t, mux, "/admin/mcp/hr/revoke", url.Values{"csrf": {"tok"}, "scope": {"team"}, "id": {team}})
if l := limits(); !slices.Equal(l.AllowedTools, []string{"jira.*"}) {
t.Fatalf("after the revoke the team grants %v", l.AllowedTools)
}
}
func TestTheMCPPageSpeaksTurkishWhenAsked(t *testing.T) {
mux, st, _ := mcpPage(t, mcpAdmin)
_ = st.PutMCPServer(context.Background(), store.MCPServer{Name: "hr", URL: "https://hr/mcp", Enabled: true})
body := peopleInTurkish(t, mux, http.MethodGet, "/admin/mcp", nil)
for _, want := range []string{"MCP sunucuları", "henüz kimse", "Sunucu ekle", "Araçlarına yetki ver"} {
if !strings.Contains(body, want) {
t.Errorf("missing %q", want)
}
}
for _, not := range []string{"nobody yet", "Add a server"} {
if strings.Contains(body, not) {
t.Errorf("English left: %q", not)
}
}
}
func TestAGrantBelowALevelThatNamesOtherToolsSaysSoAndKeepsWhatItHad(t *testing.T) {
mux, st, _ := mcpPage(t, mcpAdmin)
ctx := context.Background()
org, _ := st.CreateOrg(ctx, "Acme")
team, _ := st.CreateTeam(ctx, org, "eng")
_ = st.PutLimits(ctx, store.Limits{Scope: store.ScopeOrg, ScopeID: org, AllowedTools: []string{"hr.*"}})
_ = st.PutMCPServer(ctx, store.MCPServer{Name: "jira", URL: "https://jira/mcp", Enabled: true})
body := mirrorPost(t, mux, "/admin/mcp/jira/grant", url.Values{"csrf": {"tok"}, "scope": {"team"}, "which": {"Acme/eng"},
"tools": {"jira.*"}}).Body.String()
if !strings.Contains(body, "But organization Acme names tools and not these") {
t.Errorf("the grant should say the organization keeps jira out of reach: %s", body)
}
if l, _ := st.LimitsFor(ctx, store.ScopeTeam, team); !slices.Equal(l.AllowedTools, []string{"hr.*", "jira.*"}) {
t.Errorf("the team's list is %v; it should keep hr's, which it saw from its organization", l.AllowedTools)
}
}
func TestAnMCPServersFieldsCanBeEmptiedAndItsAddressHoldsNoPassword(t *testing.T) {
mux, st, _ := mcpPage(t, mcpAdmin)
ctx := context.Background()
_ = st.PutMCPServer(ctx, store.MCPServer{Name: "hr", URL: "https://hr/mcp", Description: "people", Enabled: true,
Timeout: 30 * time.Second, RatePerMinute: 10})
mirrorPost(t, mux, "/admin/mcp/hr/edit", url.Values{"csrf": {"tok"}, "url": {"https://hr/mcp"}, "description": {""},
"timeout": {""}, "rate": {""}})
if m, _ := st.MCPServer(ctx, "hr"); m.Description != "" || m.Timeout != 0 || m.RatePerMinute != 0 {
t.Errorf("emptied fields kept: %+v", m)
}
body := mirrorPost(t, mux, "/admin/mcp/add", url.Values{"csrf": {"tok"}, "name": {"db"},
"url": {"https://admin:hunter2@db.internal/mcp"}, "auth": {"none"}}).Body.String()
if _, err := st.MCPServer(ctx, "db"); err == nil || !strings.Contains(body, "user and password in it") {
t.Errorf("an address with a password was kept: %s", body)
}
// A grant to a key that is not there is refused, not said done.
body = mirrorPost(t, mux, "/admin/mcp/hr/grant", url.Values{"csrf": {"tok"}, "scope": {"key"}, "which": {"key-nope"},
"tools": {"hr.*"}}).Body.String()
if !strings.Contains(body, "There is no key-nope") {
t.Errorf("a grant to no key: %s", body)
}
}
func TestASignInComesBackToWhatProfileWasAskedWith(t *testing.T) {
// VS Code's sign-in opens /profile?tool=vscode&state=…; not signed in,
// that went to /login and came back to a bare /profile.
p := NewAccountPages(fakeAccounts{}, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil)))
w := httptest.NewRecorder()
p.profile(w, httptest.NewRequest("GET", "/profile?tool=vscode&state=s123", nil))
var kept *http.Cookie
for _, c := range w.Result().Cookies() {
if c.Name == afterSignInCookie {
kept = c
}
}
if w.Code != http.StatusSeeOther || kept == nil {
t.Fatalf("%d, cookie %v", w.Code, kept)
}
r := httptest.NewRequest("POST", "/login", nil)
r.AddCookie(kept)
if got := afterSignIn(httptest.NewRecorder(), r); got != "/profile?tool=vscode&state=s123" {
t.Errorf("after signing in: %q", got)
}
if got := afterSignIn(httptest.NewRecorder(), httptest.NewRequest("POST", "/login", nil)); got != "/profile" {
t.Errorf("without one: %q", got)
}
}