Files
goclaw/internal/http/channel_instance_capabilities_test.go
Duy /zuey/andGitHub 2cd750ac44 feat(channels): add context capability admin surface
Squash merge PR #115 after resolving changelog and SQLite migration-map conflicts with current dev. Renumbered channel-context PostgreSQL migration to 000075 and bumped PG required schema to 75 plus SQLite schema to 44 so it follows the run timeline migration. Local checks passed: go test ./..., go build ./..., go build -tags sqliteonly ./..., go vet ./..., and pnpm -C ui/web build. PR CI run 26705617311 passed release-versioning, go, and web.
2026-05-31 13:50:31 +07:00

211 lines
7.6 KiB
Go

package http
import (
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/crypto"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
type fakeChannelCapabilityMCPStore struct {
accessible []store.MCPAccessInfo
userCreds map[uuid.UUID]*store.MCPUserCredentials
}
func (s *fakeChannelCapabilityMCPStore) CreateServer(context.Context, *store.MCPServerData) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) GetServer(context.Context, uuid.UUID) (*store.MCPServerData, error) {
return nil, sql.ErrNoRows
}
func (s *fakeChannelCapabilityMCPStore) GetServerByName(context.Context, string) (*store.MCPServerData, error) {
return nil, sql.ErrNoRows
}
func (s *fakeChannelCapabilityMCPStore) ListServers(context.Context) ([]store.MCPServerData, error) {
return nil, nil
}
func (s *fakeChannelCapabilityMCPStore) UpdateServer(context.Context, uuid.UUID, map[string]any) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) DeleteServer(context.Context, uuid.UUID) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) GrantToAgent(context.Context, *store.MCPAgentGrant) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) RevokeFromAgent(context.Context, uuid.UUID, uuid.UUID) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) ListAgentGrants(context.Context, uuid.UUID) ([]store.MCPAgentGrant, error) {
return nil, nil
}
func (s *fakeChannelCapabilityMCPStore) ListServerGrants(context.Context, uuid.UUID) ([]store.MCPAgentGrant, error) {
return nil, nil
}
func (s *fakeChannelCapabilityMCPStore) GrantToUser(context.Context, *store.MCPUserGrant) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) RevokeFromUser(context.Context, uuid.UUID, string) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) CountAgentGrantsByServer(context.Context) (map[uuid.UUID]int, error) {
return map[uuid.UUID]int{}, nil
}
func (s *fakeChannelCapabilityMCPStore) ListAccessible(context.Context, uuid.UUID, string) ([]store.MCPAccessInfo, error) {
return s.accessible, nil
}
func (s *fakeChannelCapabilityMCPStore) CreateRequest(context.Context, *store.MCPAccessRequest) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) ListPendingRequests(context.Context) ([]store.MCPAccessRequest, error) {
return nil, nil
}
func (s *fakeChannelCapabilityMCPStore) ReviewRequest(context.Context, uuid.UUID, bool, string, string) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) GetUserCredentials(_ context.Context, serverID uuid.UUID, _ string) (*store.MCPUserCredentials, error) {
if creds := s.userCreds[serverID]; creds != nil {
return creds, nil
}
return nil, sql.ErrNoRows
}
func (s *fakeChannelCapabilityMCPStore) SetUserCredentials(context.Context, uuid.UUID, string, store.MCPUserCredentials) error {
return nil
}
func (s *fakeChannelCapabilityMCPStore) DeleteUserCredentials(context.Context, uuid.UUID, string) error {
return nil
}
type fakeChannelCapabilityCLIStore struct {
binaries []store.SecureCLIBinary
userCreds map[uuid.UUID]*store.SecureCLIUserCredential
}
func (s *fakeChannelCapabilityCLIStore) Create(context.Context, *store.SecureCLIBinary) error {
return nil
}
func (s *fakeChannelCapabilityCLIStore) Get(context.Context, uuid.UUID) (*store.SecureCLIBinary, error) {
return nil, sql.ErrNoRows
}
func (s *fakeChannelCapabilityCLIStore) Update(context.Context, uuid.UUID, map[string]any) error {
return nil
}
func (s *fakeChannelCapabilityCLIStore) Delete(context.Context, uuid.UUID) error {
return nil
}
func (s *fakeChannelCapabilityCLIStore) List(context.Context) ([]store.SecureCLIBinary, error) {
return nil, nil
}
func (s *fakeChannelCapabilityCLIStore) LookupByBinary(context.Context, string, *uuid.UUID, string) (*store.SecureCLIBinary, error) {
return nil, sql.ErrNoRows
}
func (s *fakeChannelCapabilityCLIStore) ListEnabled(context.Context) ([]store.SecureCLIBinary, error) {
return nil, nil
}
func (s *fakeChannelCapabilityCLIStore) ListForAgent(context.Context, uuid.UUID) ([]store.SecureCLIBinary, error) {
return s.binaries, nil
}
func (s *fakeChannelCapabilityCLIStore) IsRegisteredBinary(context.Context, string) (bool, error) {
return false, nil
}
func (s *fakeChannelCapabilityCLIStore) GetUserCredentials(_ context.Context, binaryID uuid.UUID, _ string) (*store.SecureCLIUserCredential, error) {
if creds := s.userCreds[binaryID]; creds != nil {
return creds, nil
}
return nil, sql.ErrNoRows
}
func (s *fakeChannelCapabilityCLIStore) SetUserCredentials(context.Context, uuid.UUID, string, []byte) error {
return nil
}
func (s *fakeChannelCapabilityCLIStore) SetUserCredentialsTyped(context.Context, uuid.UUID, string, []byte, *string, *string) error {
return nil
}
func (s *fakeChannelCapabilityCLIStore) DeleteUserCredentials(context.Context, uuid.UUID, string) error {
return nil
}
func (s *fakeChannelCapabilityCLIStore) ListUserCredentials(context.Context, uuid.UUID) ([]store.SecureCLIUserCredential, error) {
return nil, nil
}
func TestChannelContextCapabilitiesExposeOnlyCredentialMetadata(t *testing.T) {
token := "channel-capabilities-key"
setupTestCache(t, map[string]*store.APIKeyData{
crypto.HashAPIKey(token): {Scopes: []string{"operator.write"}, OwnerID: "tenant-user-1"},
})
instID := uuid.New()
agentID := uuid.New()
channelName := "discord-admin"
mcpID := uuid.New()
cliID := uuid.New()
mcpStore := &fakeChannelCapabilityMCPStore{
accessible: []store.MCPAccessInfo{{
Server: store.MCPServerData{
BaseModel: store.BaseModel{ID: mcpID},
Name: "github",
DisplayName: "GitHub",
APIKey: "global-value",
Enabled: true,
},
ToolAllow: []string{"issues.create"},
}},
userCreds: map[uuid.UUID]*store.MCPUserCredentials{
mcpID: {APIKey: "user-value"},
},
}
cliStore := &fakeChannelCapabilityCLIStore{
binaries: []store.SecureCLIBinary{{
BaseModel: store.BaseModel{ID: cliID},
BinaryName: "gh",
Description: "GitHub CLI",
EncryptedEnv: []byte(`{"VALUE":"global-value"}`),
IsGlobal: true,
Enabled: true,
}},
userCreds: map[uuid.UUID]*store.SecureCLIUserCredential{
cliID: {BinaryID: cliID, UserID: "tenant-user-1", EncryptedEnv: []byte(`{"VALUE":"user-value"}`)},
},
}
handler := NewChannelInstancesHandler(
&stubChannelInstanceStore{inst: &store.ChannelInstanceData{BaseModel: store.BaseModel{ID: instID}, Name: channelName, ChannelType: "discord", AgentID: agentID}},
nil, nil, nil, nil, nil,
)
handler.SetCapabilityStores(mcpStore, cliStore)
mux := http.NewServeMux()
handler.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/channels/instances/"+instID.String()+"/contexts/channel/"+channelName+"/capabilities", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
bodyText := rec.Body.String()
if strings.Contains(bodyText, "global-value") || strings.Contains(bodyText, "user-value") {
t.Fatalf("capability response leaked credential material: %s", bodyText)
}
var body struct {
MCP []channelCapabilityDTO `json:"mcp"`
SecureCLI []channelCapabilityDTO `json:"secure_cli"`
}
if err := json.Unmarshal([]byte(bodyText), &body); err != nil {
t.Fatal(err)
}
if len(body.MCP) != 1 || body.MCP[0].CredentialSource != "user" || !body.MCP[0].HasCredential {
t.Fatalf("mcp capability = %+v", body.MCP)
}
if len(body.SecureCLI) != 1 || body.SecureCLI[0].Source != "global" || body.SecureCLI[0].CredentialSource != "user" || !body.SecureCLI[0].HasCredential {
t.Fatalf("secure cli capability = %+v", body.SecureCLI)
}
}