diff --git a/internal/test/mock_server.go b/internal/test/mock_server.go index 36324a5e..e256f425 100644 --- a/internal/test/mock_server.go +++ b/internal/test/mock_server.go @@ -59,6 +59,10 @@ func (m *MockServer) Handle(handler http.Handler) { m.restHandlers = append(m.restHandlers, handler.ServeHTTP) } +func (m *MockServer) ResetHandlers() { + m.restHandlers = make([]http.HandlerFunc, 0) +} + func (m *MockServer) Config() *rest.Config { return m.config } diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index 9dca88e4..25e1c651 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -1,62 +1,14 @@ package mcp import ( - "context" "net/http" - "os" - "runtime" "testing" - "time" "github.com/containers/kubernetes-mcp-server/internal/test" "github.com/mark3labs/mcp-go/client/transport" - "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/suite" ) -type WatchKubeConfigSuite struct { - BaseMcpSuite -} - -func (s *WatchKubeConfigSuite) SetupTest() { - s.BaseMcpSuite.SetupTest() - kubeconfig := test.KubeConfigFake() - s.Cfg.KubeConfig = test.KubeconfigFile(s.T(), kubeconfig) -} - -func (s *WatchKubeConfigSuite) TestNotifiesToolsChange() { - if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { - s.T().Skip("Skipping test on non-Unix-like platforms") - } - // Given - s.InitMcpClient() - withTimeout, cancel := context.WithTimeout(s.T().Context(), 5*time.Second) - defer cancel() - var notification *mcp.JSONRPCNotification - s.OnNotification(func(n mcp.JSONRPCNotification) { - notification = &n - }) - // When - f, _ := os.OpenFile(s.Cfg.KubeConfig, os.O_APPEND|os.O_WRONLY, 0644) - _, _ = f.WriteString("\n") - _ = f.Close() - for notification == nil { - select { - case <-withTimeout.Done(): - s.FailNow("timeout waiting for WatchKubeConfig notification") - default: - time.Sleep(100 * time.Millisecond) - } - } - // Then - s.NotNil(notification, "WatchKubeConfig did not notify") - s.Equal("notifications/tools/list_changed", notification.Method, "WatchKubeConfig did not notify tools change") -} - -func TestWatchKubeConfig(t *testing.T) { - suite.Run(t, new(WatchKubeConfigSuite)) -} - type McpHeadersSuite struct { BaseMcpSuite mockServer *test.MockServer diff --git a/pkg/mcp/mcp_watch_test.go b/pkg/mcp/mcp_watch_test.go new file mode 100644 index 00000000..68287279 --- /dev/null +++ b/pkg/mcp/mcp_watch_test.go @@ -0,0 +1,103 @@ +package mcp + +import ( + "context" + "os" + "testing" + "time" + + "github.com/containers/kubernetes-mcp-server/internal/test" + "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/suite" +) + +type WatchKubeConfigSuite struct { + BaseMcpSuite + mockServer *test.MockServer +} + +func (s *WatchKubeConfigSuite) SetupTest() { + s.BaseMcpSuite.SetupTest() + s.mockServer = test.NewMockServer() + s.Cfg.KubeConfig = s.mockServer.KubeconfigFile(s.T()) +} + +func (s *WatchKubeConfigSuite) TearDownTest() { + s.BaseMcpSuite.TearDownTest() + if s.mockServer != nil { + s.mockServer.Close() + } +} + +func (s *WatchKubeConfigSuite) WriteKubeconfig() { + f, _ := os.OpenFile(s.Cfg.KubeConfig, os.O_APPEND|os.O_WRONLY, 0644) + _, _ = f.WriteString("\n") + _ = f.Close() +} + +// WaitForNotification waits for an MCP server notification or fails the test after a timeout +func (s *WatchKubeConfigSuite) WaitForNotification() *mcp.JSONRPCNotification { + withTimeout, cancel := context.WithTimeout(s.T().Context(), 5*time.Second) + defer cancel() + var notification *mcp.JSONRPCNotification + s.OnNotification(func(n mcp.JSONRPCNotification) { + notification = &n + }) + for notification == nil { + select { + case <-withTimeout.Done(): + s.FailNow("timeout waiting for WatchKubeConfig notification") + default: + time.Sleep(100 * time.Millisecond) + } + } + return notification +} + +func (s *WatchKubeConfigSuite) TestNotifiesToolsChange() { + // Given + s.InitMcpClient() + // When + s.WriteKubeconfig() + notification := s.WaitForNotification() + // Then + s.NotNil(notification, "WatchKubeConfig did not notify") + s.Equal("notifications/tools/list_changed", notification.Method, "WatchKubeConfig did not notify tools change") +} + +func (s *WatchKubeConfigSuite) TestClearsNoLongerAvailableTools() { + s.mockServer.Handle(&test.InOpenShiftHandler{}) + s.InitMcpClient() + + s.Run("OpenShift tool is available", func() { + tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) + s.Require().NoError(err, "call ListTools failed") + s.Require().NotNil(tools, "list tools failed") + var found bool + for _, tool := range tools.Tools { + if tool.Name == "projects_list" { + found = true + break + } + } + s.Truef(found, "expected OpenShift tool to be available") + }) + + s.Run("OpenShift tool is removed after kubeconfig change", func() { + // Reload Config without OpenShift + s.mockServer.ResetHandlers() + s.WriteKubeconfig() + s.WaitForNotification() + + tools, err := s.ListTools(s.T().Context(), mcp.ListToolsRequest{}) + s.Require().NoError(err, "call ListTools failed") + s.Require().NotNil(tools, "list tools failed") + for _, tool := range tools.Tools { + s.Require().Falsef(tool.Name == "projects_list", "expected OpenShift tool to be removed") + } + }) +} + +func TestWatchKubeConfig(t *testing.T) { + suite.Run(t, new(WatchKubeConfigSuite)) +}