package controller import ( "encoding/json" "testing" "time" "github.com/stretchr/testify/require" "github.com/nezhahq/nezha/model" "github.com/nezhahq/nezha/service/singleton" ) func TestServerList_FiltersByPermission(t *testing.T) { cleanup, uid := setupMCPTest(t) defer cleanup() srv2 := &model.Server{} srv2.ID = 8 srv2.Name = "beta" srv2.SetUserID(999) singleton.ServerShared.InsertForTest(srv2) tok, _ := mkToken(t, uid, []string{model.ScopeInventoryRead}, nil) c, w := mcpCallCtx(t, tok, uid, jsonRPCRequest{ JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "tools/call", Params: jsonObj(t, toolCallParams{Name: "server.list", Arguments: json.RawMessage("{}")}), }) mcpEndpoint(c) _, tcr := decodeRPC(w) require.False(t, tcr.IsError) rows := decodeServerListRows(t, tcr.StructuredContent) require.Len(t, rows, 1, "must filter out non-owned server") require.EqualValues(t, 7, rows[0]["id"]) } // decodeServerListRows unwraps the {servers,count} object that server.list now // returns (MCP requires structuredContent to be an object, not a bare array) // and asserts count stays in sync with the servers slice length. func decodeServerListRows(t *testing.T, structured any) []map[string]any { t.Helper() rb, _ := json.Marshal(structured) var res struct { Servers []map[string]any `json:"servers"` Count int `json:"count"` } require.NoError(t, json.Unmarshal(rb, &res)) require.Equal(t, len(res.Servers), res.Count, "count must match servers length") return res.Servers } func TestServerList_ServerWhitelistFurtherFiltering(t *testing.T) { cleanup, uid := setupMCPTest(t) defer cleanup() srv2 := &model.Server{} srv2.ID = 8 srv2.Name = "beta" srv2.SetUserID(uid) singleton.ServerShared.InsertForTest(srv2) tok, _ := mkToken(t, uid, []string{model.ScopeInventoryRead}, []uint64{8}) c, w := mcpCallCtx(t, tok, uid, jsonRPCRequest{ JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "tools/call", Params: jsonObj(t, toolCallParams{Name: "server.list", Arguments: json.RawMessage("{}")}), }) mcpEndpoint(c) _, tcr := decodeRPC(w) require.False(t, tcr.IsError) rows := decodeServerListRows(t, tcr.StructuredContent) require.Len(t, rows, 1) require.EqualValues(t, 8, rows[0]["id"]) } func TestServerList_OnlineOnlyFilter(t *testing.T) { cleanup, uid := setupMCPTest(t) defer cleanup() srv, _ := singleton.ServerShared.Get(7) require.NotNil(t, srv) srv.LastActive = time.Now() tok, _ := mkToken(t, uid, []string{model.ScopeInventoryRead}, nil) c, w := mcpCallCtx(t, tok, uid, jsonRPCRequest{ JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "tools/call", Params: jsonObj(t, toolCallParams{Name: "server.list", Arguments: jsonRaw(map[string]any{"online_only": true})}), }) mcpEndpoint(c) _, tcr := decodeRPC(w) require.False(t, tcr.IsError) rows := decodeServerListRows(t, tcr.StructuredContent) require.Len(t, rows, 1) } func TestServerGet_RequiresServerID(t *testing.T) { cleanup, uid := setupMCPTest(t) defer cleanup() tok, _ := mkToken(t, uid, []string{model.ScopeServerRead}, nil) c, w := mcpCallCtx(t, tok, uid, jsonRPCRequest{ JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "tools/call", Params: jsonObj(t, toolCallParams{Name: "server.get", Arguments: json.RawMessage("{}")}), }) mcpEndpoint(c) _, tcr := decodeRPC(w) require.True(t, tcr.IsError) require.Contains(t, tcr.Content[0].Text, "server_id required") } func TestServerExec_ScopeMissing(t *testing.T) { cleanup, uid := setupMCPTest(t) defer cleanup() tok, _ := mkToken(t, uid, []string{model.ScopeServerRead}, nil) c, w := mcpCallCtx(t, tok, uid, jsonRPCRequest{ JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "tools/call", Params: jsonObj(t, toolCallParams{Name: "server.exec", Arguments: jsonRaw(map[string]any{"server_id": 7, "cmd": "echo"})}), }) mcpEndpoint(c) _, tcr := decodeRPC(w) require.True(t, tcr.IsError) require.Contains(t, tcr.Content[0].Text, "nezha:server:exec") }