test: extract mock-server for reutilization (#247)

Signed-off-by: Marc Nuri <marc@marcnuri.com>
This commit is contained in:
Marc Nuri
2025-08-07 15:32:20 +03:00
committed by GitHub
parent 9ec5c829db
commit 43744f2978
5 changed files with 47 additions and 37 deletions

View File

@@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/containers/kubernetes-mcp-server/internal/test"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
@@ -48,10 +49,10 @@ func TestWatchKubeConfig(t *testing.T) {
}
func TestSseHeaders(t *testing.T) {
mockServer := NewMockServer()
mockServer := test.NewMockServer()
defer mockServer.Close()
before := func(c *mcpContext) {
c.withKubeConfig(mockServer.config)
c.withKubeConfig(mockServer.Config())
c.clientOptions = append(c.clientOptions, client.WithHeaders(map[string]string{"kubernetes-authorization": "Bearer a-token-from-mcp-client"}))
}
pathHeaders := make(map[string]http.Header, 0)

View File

@@ -1,151 +0,0 @@
package mcp
import (
"encoding/json"
"errors"
"io"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
"k8s.io/client-go/rest"
"net/http"
"net/http/httptest"
)
type MockServer struct {
server *httptest.Server
config *rest.Config
restHandlers []http.HandlerFunc
}
func NewMockServer() *MockServer {
ms := &MockServer{}
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
ms.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
for _, handler := range ms.restHandlers {
handler(w, req)
}
}))
ms.config = &rest.Config{
Host: ms.server.URL,
APIPath: "/api",
ContentConfig: rest.ContentConfig{
NegotiatedSerializer: codecs,
ContentType: runtime.ContentTypeJSON,
GroupVersion: &v1.SchemeGroupVersion,
},
}
ms.restHandlers = make([]http.HandlerFunc, 0)
return ms
}
func (m *MockServer) Close() {
m.server.Close()
}
func (m *MockServer) Handle(handler http.Handler) {
m.restHandlers = append(m.restHandlers, handler.ServeHTTP)
}
func writeObject(w http.ResponseWriter, obj runtime.Object) {
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
if err := json.NewEncoder(w).Encode(obj); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
type streamAndReply struct {
httpstream.Stream
replySent <-chan struct{}
}
type streamContext struct {
conn io.Closer
stdinStream io.ReadCloser
stdoutStream io.WriteCloser
stderrStream io.WriteCloser
writeStatus func(status *apierrors.StatusError) error
}
type StreamOptions struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
func v4WriteStatusFunc(stream io.Writer) func(status *apierrors.StatusError) error {
return func(status *apierrors.StatusError) error {
bs, err := json.Marshal(status.Status())
if err != nil {
return err
}
_, err = stream.Write(bs)
return err
}
}
func createHTTPStreams(w http.ResponseWriter, req *http.Request, opts *StreamOptions) (*streamContext, error) {
_, err := httpstream.Handshake(req, w, []string{"v4.channel.k8s.io"})
if err != nil {
return nil, err
}
upgrader := spdy.NewResponseUpgrader()
streamCh := make(chan streamAndReply)
conn := upgrader.UpgradeResponse(w, req, func(stream httpstream.Stream, replySent <-chan struct{}) error {
streamCh <- streamAndReply{Stream: stream, replySent: replySent}
return nil
})
ctx := &streamContext{
conn: conn,
}
// wait for stream
replyChan := make(chan struct{}, 4)
defer close(replyChan)
receivedStreams := 0
expectedStreams := 1
if opts.Stdout != nil {
expectedStreams++
}
if opts.Stdin != nil {
expectedStreams++
}
if opts.Stderr != nil {
expectedStreams++
}
WaitForStreams:
for {
select {
case stream := <-streamCh:
streamType := stream.Headers().Get(v1.StreamType)
switch streamType {
case v1.StreamTypeError:
replyChan <- struct{}{}
ctx.writeStatus = v4WriteStatusFunc(stream)
case v1.StreamTypeStdout:
replyChan <- struct{}{}
ctx.stdoutStream = stream
case v1.StreamTypeStdin:
replyChan <- struct{}{}
ctx.stdinStream = stream
case v1.StreamTypeStderr:
replyChan <- struct{}{}
ctx.stderrStream = stream
default:
// add other stream ...
return nil, errors.New("unimplemented stream type")
}
case <-replyChan:
receivedStreams++
if receivedStreams == expectedStreams {
break WaitForStreams
}
}
}
return ctx, nil
}

View File

@@ -2,27 +2,29 @@ package mcp
import (
"bytes"
"github.com/containers/kubernetes-mcp-server/pkg/config"
"github.com/mark3labs/mcp-go/mcp"
"io"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"
"strings"
"testing"
"github.com/containers/kubernetes-mcp-server/internal/test"
"github.com/containers/kubernetes-mcp-server/pkg/config"
"github.com/mark3labs/mcp-go/mcp"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestPodsExec(t *testing.T) {
testCase(t, func(c *mcpContext) {
mockServer := NewMockServer()
mockServer := test.NewMockServer()
defer mockServer.Close()
c.withKubeConfig(mockServer.config)
c.withKubeConfig(mockServer.Config())
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/api/v1/namespaces/default/pods/pod-to-exec/exec" {
return
}
var stdin, stdout bytes.Buffer
ctx, err := createHTTPStreams(w, req, &StreamOptions{
ctx, err := test.CreateHTTPStreams(w, req, &test.StreamOptions{
Stdin: &stdin,
Stdout: &stdout,
})
@@ -31,15 +33,15 @@ func TestPodsExec(t *testing.T) {
_, _ = w.Write([]byte(err.Error()))
return
}
defer func(conn io.Closer) { _ = conn.Close() }(ctx.conn)
_, _ = io.WriteString(ctx.stdoutStream, "command:"+strings.Join(req.URL.Query()["command"], " ")+"\n")
_, _ = io.WriteString(ctx.stdoutStream, "container:"+strings.Join(req.URL.Query()["container"], " ")+"\n")
defer func(conn io.Closer) { _ = conn.Close() }(ctx.Closer)
_, _ = io.WriteString(ctx.StdoutStream, "command:"+strings.Join(req.URL.Query()["command"], " ")+"\n")
_, _ = io.WriteString(ctx.StdoutStream, "container:"+strings.Join(req.URL.Query()["container"], " ")+"\n")
}))
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/api/v1/namespaces/default/pods/pod-to-exec" {
return
}
writeObject(w, &v1.Pod{
test.WriteObject(w, &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pod-to-exec",

View File

@@ -1,12 +1,13 @@
package mcp
import (
"github.com/containers/kubernetes-mcp-server/pkg/config"
"github.com/containers/kubernetes-mcp-server/pkg/output"
"regexp"
"strings"
"testing"
"github.com/containers/kubernetes-mcp-server/pkg/config"
"github.com/containers/kubernetes-mcp-server/pkg/output"
"github.com/mark3labs/mcp-go/mcp"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"

View File

@@ -5,6 +5,7 @@ import (
"regexp"
"testing"
"github.com/containers/kubernetes-mcp-server/internal/test"
"github.com/mark3labs/mcp-go/mcp"
"github.com/containers/kubernetes-mcp-server/pkg/config"
@@ -12,9 +13,9 @@ import (
func TestPodsTopMetricsUnavailable(t *testing.T) {
testCase(t, func(c *mcpContext) {
mockServer := NewMockServer()
mockServer := test.NewMockServer()
defer mockServer.Close()
c.withKubeConfig(mockServer.config)
c.withKubeConfig(mockServer.Config())
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
@@ -45,9 +46,9 @@ func TestPodsTopMetricsUnavailable(t *testing.T) {
func TestPodsTopMetricsAvailable(t *testing.T) {
testCase(t, func(c *mcpContext) {
mockServer := NewMockServer()
mockServer := test.NewMockServer()
defer mockServer.Close()
c.withKubeConfig(mockServer.config)
c.withKubeConfig(mockServer.Config())
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
println("Request received:", req.Method, req.URL.Path) // TODO: REMOVE LINE
w.Header().Set("Content-Type", "application/json")
@@ -211,9 +212,9 @@ func TestPodsTopMetricsAvailable(t *testing.T) {
func TestPodsTopDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Group: "metrics.k8s.io", Version: "v1beta1"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
mockServer := NewMockServer()
mockServer := test.NewMockServer()
defer mockServer.Close()
c.withKubeConfig(mockServer.config)
c.withKubeConfig(mockServer.Config())
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)