diff --git a/pkg/mcp/common_test.go b/pkg/mcp/common_test.go index 1aa118a..8706489 100644 --- a/pkg/mcp/common_test.go +++ b/pkg/mcp/common_test.go @@ -429,6 +429,7 @@ type BaseMcpSuite struct { func (s *BaseMcpSuite) SetupTest() { s.Cfg = config.Default() + s.Cfg.ListOutput = "yaml" s.Cfg.KubeConfig = filepath.Join(s.T().TempDir(), "config") s.Require().NoError(os.WriteFile(s.Cfg.KubeConfig, envTest.KubeConfig, 0600), "Expected to write kubeconfig") } diff --git a/pkg/mcp/events_test.go b/pkg/mcp/events_test.go index 578be42..8e0f4f4 100644 --- a/pkg/mcp/events_test.go +++ b/pkg/mcp/events_test.go @@ -107,15 +107,15 @@ func (s *EventsSuite) TestEventsListDenied() { `), s.Cfg), "Expected to parse denied resources config") s.InitMcpClient() s.Run("events_list (denied)", func() { - eventList, err := s.CallTool("events_list", map[string]interface{}{}) - s.Run("events_list has error", func() { - s.Truef(eventList.IsError, "call tool should fail") + toolResult, err := s.CallTool("events_list", map[string]interface{}{}) + s.Run("has error", func() { + s.Truef(toolResult.IsError, "call tool should fail") s.Nilf(err, "call tool should not return error object") }) - s.Run("events_list describes denial", func() { + s.Run("describes denial", func() { expectedMessage := "failed to list events in all namespaces: resource not allowed: /v1, Kind=Event" - s.Equalf(expectedMessage, eventList.Content[0].(mcp.TextContent).Text, - "expected descriptive error '%s', got %v", expectedMessage, eventList.Content[0].(mcp.TextContent).Text) + s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text, + "expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text) }) }) } diff --git a/pkg/mcp/namespaces_test.go b/pkg/mcp/namespaces_test.go index e7c7170..26c92ee 100644 --- a/pkg/mcp/namespaces_test.go +++ b/pkg/mcp/namespaces_test.go @@ -5,7 +5,9 @@ import ( "slices" "testing" + "github.com/BurntSushi/toml" "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -14,108 +16,100 @@ import ( "github.com/containers/kubernetes-mcp-server/internal/test" "github.com/containers/kubernetes-mcp-server/pkg/config" - "github.com/containers/kubernetes-mcp-server/pkg/output" ) -func TestNamespacesList(t *testing.T) { - testCase(t, func(c *mcpContext) { - c.withEnvTest() - toolResult, err := c.callTool("namespaces_list", map[string]interface{}{}) - t.Run("namespaces_list returns namespace list", func(t *testing.T) { - if err != nil { - t.Fatalf("call tool failed %v", err) - } - if toolResult.IsError { - t.Fatalf("call tool failed") - } +type NamespacesSuite struct { + BaseMcpSuite +} + +func (s *NamespacesSuite) TestNamespacesList() { + s.InitMcpClient() + s.Run("namespaces_list", func() { + toolResult, err := s.CallTool("namespaces_list", map[string]interface{}{}) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(toolResult.IsError, "call tool failed") }) + s.Require().NotNil(toolResult, "Expected tool result from call") var decoded []unstructured.Unstructured err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded) - t.Run("namespaces_list has yaml content", func(t *testing.T) { - if err != nil { - t.Fatalf("invalid tool result content %v", err) - } + s.Run("has yaml content", func() { + s.Nilf(err, "invalid tool result content %v", err) }) - t.Run("namespaces_list returns at least 3 items", func(t *testing.T) { - if len(decoded) < 3 { - t.Errorf("invalid namespace count, expected at least 3, got %v", len(decoded)) - } + s.Run("returns at least 3 items", func() { + s.Truef(len(decoded) >= 3, "expected at least 3 items, got %v", len(decoded)) for _, expectedNamespace := range []string{"default", "ns-1", "ns-2"} { - idx := slices.IndexFunc(decoded, func(ns unstructured.Unstructured) bool { + s.Truef(slices.ContainsFunc(decoded, func(ns unstructured.Unstructured) bool { return ns.GetName() == expectedNamespace - }) - if idx == -1 { - t.Errorf("namespace %s not found in the list", expectedNamespace) - } + }), "namespace %s not found in the list", expectedNamespace) } }) }) } -func TestNamespacesListDenied(t *testing.T) { - deniedResourcesServer := test.Must(config.ReadToml([]byte(` +func (s *NamespacesSuite) TestNamespacesListDenied() { + s.Require().NoError(toml.Unmarshal([]byte(` denied_resources = [ { version = "v1", kind = "Namespace" } ] - `))) - testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) { - c.withEnvTest() - namespacesList, _ := c.callTool("namespaces_list", map[string]interface{}{}) - t.Run("namespaces_list has error", func(t *testing.T) { - if !namespacesList.IsError { - t.Fatalf("call tool should fail") - } + `), s.Cfg), "Expected to parse denied resources config") + s.InitMcpClient() + s.Run("namespaces_list (denied)", func() { + toolResult, err := s.CallTool("namespaces_list", map[string]interface{}{}) + s.Run("has error", func() { + s.Truef(toolResult.IsError, "call tool should fail") + s.Nilf(err, "call tool should not return error object") }) - t.Run("namespaces_list describes denial", func(t *testing.T) { + s.Run("describes denial", func() { expectedMessage := "failed to list namespaces: resource not allowed: /v1, Kind=Namespace" - if namespacesList.Content[0].(mcp.TextContent).Text != expectedMessage { - t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, namespacesList.Content[0].(mcp.TextContent).Text) - } + s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text, + "expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text) }) }) } -func TestNamespacesListAsTable(t *testing.T) { - testCaseWithContext(t, &mcpContext{listOutput: output.Table}, func(c *mcpContext) { - c.withEnvTest() - toolResult, err := c.callTool("namespaces_list", map[string]interface{}{}) - t.Run("namespaces_list returns namespace list", func(t *testing.T) { - if err != nil { - t.Fatalf("call tool failed %v", err) - } - if toolResult.IsError { - t.Fatalf("call tool failed") - } +func (s *NamespacesSuite) TestNamespacesListAsTable() { + s.Cfg.ListOutput = "table" + s.InitMcpClient() + s.Run("namespaces_list (list_output=table)", func() { + toolResult, err := s.CallTool("namespaces_list", map[string]interface{}{}) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(toolResult.IsError, "call tool failed") }) + s.Require().NotNil(toolResult, "Expected tool result from call") out := toolResult.Content[0].(mcp.TextContent).Text - t.Run("namespaces_list returns column headers", func(t *testing.T) { + s.Run("returns column headers", func() { expectedHeaders := "APIVERSION\\s+KIND\\s+NAME\\s+STATUS\\s+AGE\\s+LABELS" - if m, e := regexp.MatchString(expectedHeaders, out); !m || e != nil { - t.Fatalf("Expected headers '%s' not found in output:\n%s", expectedHeaders, out) - } + m, e := regexp.MatchString(expectedHeaders, out) + s.Truef(m, "Expected headers '%s' not found in output:\n%s", expectedHeaders, out) + s.NoErrorf(e, "Error matching headers regex: %v", e) }) - t.Run("namespaces_list returns formatted row for ns-1", func(t *testing.T) { + s.Run("returns formatted row for ns-1", func() { expectedRow := "(?v1)\\s+" + "(?Namespace)\\s+" + "(?ns-1)\\s+" + "(?Active)\\s+" + "(?(\\d+m)?(\\d+s)?)\\s+" + "(?kubernetes.io/metadata.name=ns-1)" - if m, e := regexp.MatchString(expectedRow, out); !m || e != nil { - t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, out) - } + m, e := regexp.MatchString(expectedRow, out) + s.Truef(m, "Expected row '%s' not found in output:\n%s", expectedRow, out) + s.NoErrorf(e, "Error matching ns-1 regex: %v", e) }) - t.Run("namespaces_list returns formatted row for ns-2", func(t *testing.T) { + s.Run("returns formatted row for ns-2", func() { expectedRow := "(?v1)\\s+" + "(?Namespace)\\s+" + "(?ns-2)\\s+" + "(?Active)\\s+" + "(?(\\d+m)?(\\d+s)?)\\s+" + "(?kubernetes.io/metadata.name=ns-2)" - if m, e := regexp.MatchString(expectedRow, out); !m || e != nil { - t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, out) - } + m, e := regexp.MatchString(expectedRow, out) + s.Truef(m, "Expected row '%s' not found in output:\n%s", expectedRow, out) + s.NoErrorf(e, "Error matching ns-2 regex: %v", e) }) }) +} +func TestNamespaces(t *testing.T) { + suite.Run(t, new(NamespacesSuite)) } func TestProjectsListInOpenShift(t *testing.T) {