Skip to content

Commit d6936f4

Browse files
authored
test(mcp): refactor events toolset tests (#328)
Signed-off-by: Marc Nuri <[email protected]>
1 parent f496c64 commit d6936f4

File tree

3 files changed

+126
-110
lines changed

3 files changed

+126
-110
lines changed

pkg/mcp/common_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/mark3labs/mcp-go/server"
2121
"github.com/pkg/errors"
2222
"github.com/spf13/afero"
23+
"github.com/stretchr/testify/suite"
2324
"golang.org/x/sync/errgroup"
2425
corev1 "k8s.io/api/core/v1"
2526
rbacv1 "k8s.io/api/rbac/v1"
@@ -418,3 +419,32 @@ func createTestData(ctx context.Context) {
418419
_, _ = kubernetesAdmin.CoreV1().ConfigMaps("default").
419420
Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "a-configmap-to-delete"}}, metav1.CreateOptions{})
420421
}
422+
423+
type BaseMcpSuite struct {
424+
suite.Suite
425+
*test.McpClient
426+
mcpServer *Server
427+
Cfg *config.StaticConfig
428+
}
429+
430+
func (s *BaseMcpSuite) SetupTest() {
431+
s.Cfg = config.Default()
432+
s.Cfg.KubeConfig = filepath.Join(s.T().TempDir(), "config")
433+
s.Require().NoError(os.WriteFile(s.Cfg.KubeConfig, envTest.KubeConfig, 0600), "Expected to write kubeconfig")
434+
}
435+
436+
func (s *BaseMcpSuite) TearDownTest() {
437+
if s.McpClient != nil {
438+
s.McpClient.Close()
439+
}
440+
if s.mcpServer != nil {
441+
s.mcpServer.Close()
442+
}
443+
}
444+
445+
func (s *BaseMcpSuite) InitMcpClient() {
446+
var err error
447+
s.mcpServer, err = NewServer(Configuration{StaticConfig: s.Cfg})
448+
s.Require().NoError(err, "Expected no error creating MCP server")
449+
s.McpClient = test.NewMcpClient(s.T(), s.mcpServer.ServeHTTP(nil))
450+
}

pkg/mcp/configuration_test.go

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,22 @@ import (
1010
"sigs.k8s.io/yaml"
1111

1212
"github.com/containers/kubernetes-mcp-server/internal/test"
13-
"github.com/containers/kubernetes-mcp-server/pkg/config"
1413
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
1514
)
1615

1716
type ConfigurationSuite struct {
18-
suite.Suite
19-
*test.McpClient
20-
mcpServer *Server
21-
Cfg *config.StaticConfig
17+
BaseMcpSuite
2218
}
2319

2420
func (s *ConfigurationSuite) SetupTest() {
25-
s.Cfg = config.Default()
26-
}
27-
28-
func (s *ConfigurationSuite) TearDownTest() {
29-
if s.McpClient != nil {
30-
s.McpClient.Close()
31-
}
32-
if s.mcpServer != nil {
33-
s.mcpServer.Close()
34-
}
35-
}
36-
37-
func (s *ConfigurationSuite) InitMcpClient() {
38-
var err error
39-
s.mcpServer, err = NewServer(Configuration{StaticConfig: s.Cfg})
40-
s.Require().NoError(err, "Expected no error creating MCP server")
41-
s.McpClient = test.NewMcpClient(s.T(), s.mcpServer.ServeHTTP(nil))
42-
}
43-
44-
func (s *ConfigurationSuite) TestConfigurationView() {
45-
// Out of cluster requires kubeconfig
21+
s.BaseMcpSuite.SetupTest()
22+
// Use mock server for predictable kubeconfig content
4623
mockServer := test.NewMockServer()
4724
s.T().Cleanup(mockServer.Close)
4825
s.Cfg.KubeConfig = mockServer.KubeconfigFile(s.T())
26+
}
27+
28+
func (s *ConfigurationSuite) TestConfigurationView() {
4929
s.InitMcpClient()
5030
s.Run("configuration_view", func() {
5131
toolResult, err := s.CallTool("configuration_view", map[string]interface{}{})
@@ -108,6 +88,7 @@ func (s *ConfigurationSuite) TestConfigurationView() {
10888
}
10989

11090
func (s *ConfigurationSuite) TestConfigurationViewInCluster() {
91+
s.Cfg.KubeConfig = "" // Force in-cluster
11192
kubernetes.InClusterConfig = func() (*rest.Config, error) {
11293
return &rest.Config{
11394
Host: "https://kubernetes.default.svc",

pkg/mcp/events_test.go

Lines changed: 89 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,34 @@ package mcp
33
import (
44
"testing"
55

6+
"github.com/BurntSushi/toml"
67
"github.com/mark3labs/mcp-go/mcp"
8+
"github.com/stretchr/testify/suite"
79
v1 "k8s.io/api/core/v1"
810
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9-
10-
"github.com/containers/kubernetes-mcp-server/internal/test"
11-
"github.com/containers/kubernetes-mcp-server/pkg/config"
11+
"k8s.io/client-go/kubernetes"
1212
)
1313

14-
func TestEventsList(t *testing.T) {
15-
testCase(t, func(c *mcpContext) {
16-
c.withEnvTest()
17-
toolResult, err := c.callTool("events_list", map[string]interface{}{})
18-
t.Run("events_list with no events returns OK", func(t *testing.T) {
19-
if err != nil {
20-
t.Fatalf("call tool failed %v", err)
21-
}
22-
if toolResult.IsError {
23-
t.Fatalf("call tool failed")
24-
}
25-
if toolResult.Content[0].(mcp.TextContent).Text != "No events found" {
26-
t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
27-
}
14+
type EventsSuite struct {
15+
BaseMcpSuite
16+
}
17+
18+
func (s *EventsSuite) TestEventsList() {
19+
s.InitMcpClient()
20+
s.Run("events_list (no events)", func() {
21+
toolResult, err := s.CallTool("events_list", map[string]interface{}{})
22+
s.Run("no error", func() {
23+
s.Nilf(err, "call tool failed %v", err)
24+
s.Falsef(toolResult.IsError, "call tool failed")
2825
})
29-
client := c.newKubernetesClient()
26+
s.Run("returns no events message", func() {
27+
s.Equal("No events found", toolResult.Content[0].(mcp.TextContent).Text)
28+
})
29+
})
30+
s.Run("events_list (with events)", func() {
31+
client := kubernetes.NewForConfigOrDie(envTestRestConfig)
3032
for _, ns := range []string{"default", "ns-1"} {
31-
_, _ = client.CoreV1().Events(ns).Create(c.ctx, &v1.Event{
33+
_, _ = client.CoreV1().Events(ns).Create(s.T().Context(), &v1.Event{
3234
ObjectMeta: metav1.ObjectMeta{
3335
Name: "an-event-in-" + ns,
3436
},
@@ -42,79 +44,82 @@ func TestEventsList(t *testing.T) {
4244
Message: "The event message",
4345
}, metav1.CreateOptions{})
4446
}
45-
toolResult, err = c.callTool("events_list", map[string]interface{}{})
46-
t.Run("events_list with events returns all OK", func(t *testing.T) {
47-
if err != nil {
48-
t.Fatalf("call tool failed %v", err)
49-
}
50-
if toolResult.IsError {
51-
t.Fatalf("call tool failed")
52-
}
53-
if toolResult.Content[0].(mcp.TextContent).Text != "The following events (YAML format) were found:\n"+
54-
"- InvolvedObject:\n"+
55-
" Kind: Pod\n"+
56-
" Name: a-pod\n"+
57-
" apiVersion: v1\n"+
58-
" Message: The event message\n"+
59-
" Namespace: default\n"+
60-
" Reason: \"\"\n"+
61-
" Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
62-
" Type: Normal\n"+
63-
"- InvolvedObject:\n"+
64-
" Kind: Pod\n"+
65-
" Name: a-pod\n"+
66-
" apiVersion: v1\n"+
67-
" Message: The event message\n"+
68-
" Namespace: ns-1\n"+
69-
" Reason: \"\"\n"+
70-
" Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
71-
" Type: Normal\n" {
72-
t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
73-
}
74-
})
75-
toolResult, err = c.callTool("events_list", map[string]interface{}{
76-
"namespace": "ns-1",
47+
s.Run("events_list()", func() {
48+
toolResult, err := s.CallTool("events_list", map[string]interface{}{})
49+
s.Run("no error", func() {
50+
s.Nilf(err, "call tool failed %v", err)
51+
s.Falsef(toolResult.IsError, "call tool failed")
52+
})
53+
s.Run("returns all events", func() {
54+
s.Equalf("The following events (YAML format) were found:\n"+
55+
"- InvolvedObject:\n"+
56+
" Kind: Pod\n"+
57+
" Name: a-pod\n"+
58+
" apiVersion: v1\n"+
59+
" Message: The event message\n"+
60+
" Namespace: default\n"+
61+
" Reason: \"\"\n"+
62+
" Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
63+
" Type: Normal\n"+
64+
"- InvolvedObject:\n"+
65+
" Kind: Pod\n"+
66+
" Name: a-pod\n"+
67+
" apiVersion: v1\n"+
68+
" Message: The event message\n"+
69+
" Namespace: ns-1\n"+
70+
" Reason: \"\"\n"+
71+
" Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
72+
" Type: Normal\n",
73+
toolResult.Content[0].(mcp.TextContent).Text,
74+
"unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
75+
76+
})
7777
})
78-
t.Run("events_list in namespace with events returns from namespace OK", func(t *testing.T) {
79-
if err != nil {
80-
t.Fatalf("call tool failed %v", err)
81-
}
82-
if toolResult.IsError {
83-
t.Fatalf("call tool failed")
84-
}
85-
if toolResult.Content[0].(mcp.TextContent).Text != "The following events (YAML format) were found:\n"+
86-
"- InvolvedObject:\n"+
87-
" Kind: Pod\n"+
88-
" Name: a-pod\n"+
89-
" apiVersion: v1\n"+
90-
" Message: The event message\n"+
91-
" Namespace: ns-1\n"+
92-
" Reason: \"\"\n"+
93-
" Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
94-
" Type: Normal\n" {
95-
t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
96-
}
78+
s.Run("events_list(namespace=ns-1)", func() {
79+
toolResult, err := s.CallTool("events_list", map[string]interface{}{
80+
"namespace": "ns-1",
81+
})
82+
s.Run("no error", func() {
83+
s.Nilf(err, "call tool failed %v", err)
84+
s.Falsef(toolResult.IsError, "call tool failed")
85+
})
86+
s.Run("returns events from namespace", func() {
87+
s.Equalf("The following events (YAML format) were found:\n"+
88+
"- InvolvedObject:\n"+
89+
" Kind: Pod\n"+
90+
" Name: a-pod\n"+
91+
" apiVersion: v1\n"+
92+
" Message: The event message\n"+
93+
" Namespace: ns-1\n"+
94+
" Reason: \"\"\n"+
95+
" Timestamp: 0001-01-01 00:00:00 +0000 UTC\n"+
96+
" Type: Normal\n",
97+
toolResult.Content[0].(mcp.TextContent).Text,
98+
"unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
99+
})
97100
})
98101
})
99102
}
100103

101-
func TestEventsListDenied(t *testing.T) {
102-
deniedResourcesServer := test.Must(config.ReadToml([]byte(`
104+
func (s *EventsSuite) TestEventsListDenied() {
105+
s.Require().NoError(toml.Unmarshal([]byte(`
103106
denied_resources = [ { version = "v1", kind = "Event" } ]
104-
`)))
105-
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
106-
c.withEnvTest()
107-
eventList, _ := c.callTool("events_list", map[string]interface{}{})
108-
t.Run("events_list has error", func(t *testing.T) {
109-
if !eventList.IsError {
110-
t.Fatalf("call tool should fail")
111-
}
107+
`), s.Cfg), "Expected to parse denied resources config")
108+
s.InitMcpClient()
109+
s.Run("events_list (denied)", func() {
110+
eventList, err := s.CallTool("events_list", map[string]interface{}{})
111+
s.Run("events_list has error", func() {
112+
s.Truef(eventList.IsError, "call tool should fail")
113+
s.Nilf(err, "call tool should not return error object")
112114
})
113-
t.Run("events_list describes denial", func(t *testing.T) {
115+
s.Run("events_list describes denial", func() {
114116
expectedMessage := "failed to list events in all namespaces: resource not allowed: /v1, Kind=Event"
115-
if eventList.Content[0].(mcp.TextContent).Text != expectedMessage {
116-
t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, eventList.Content[0].(mcp.TextContent).Text)
117-
}
117+
s.Equalf(expectedMessage, eventList.Content[0].(mcp.TextContent).Text,
118+
"expected descriptive error '%s', got %v", expectedMessage, eventList.Content[0].(mcp.TextContent).Text)
118119
})
119120
})
120121
}
122+
123+
func TestEvents(t *testing.T) {
124+
suite.Run(t, new(EventsSuite))
125+
}

0 commit comments

Comments
 (0)