Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/test/mock_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is similar code above when creating the server

}

func (m *MockServer) Config() *rest.Config {
return m.config
}
Expand Down
48 changes: 0 additions & 48 deletions pkg/mcp/mcp_test.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
103 changes: 103 additions & 0 deletions pkg/mcp/mcp_watch_test.go
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a bit, to see that it actually "reset" the handlers (while the other one accepts one handler).

I guess no major issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try to make it clearer.
This was the easiest path I found to test the functionality.

I will probably improve the test with a custom provider to verify other behaviors such as the Provider close if exists:

// close the old provider
if s.p != nil {
s.p.Close()
}

and so on.

Bu It don't want to spend too much time on this at the moment since it's blocking #385

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))
}
Loading