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