Skip to content

Commit 62bd582

Browse files
authored
test(mcp): refactored OpenShift related tests to use testify (#438)
Signed-off-by: Marc Nuri <[email protected]>
1 parent 711239b commit 62bd582

File tree

5 files changed

+211
-264
lines changed

5 files changed

+211
-264
lines changed

pkg/mcp/common_test.go

Lines changed: 83 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -222,120 +222,6 @@ func (c *mcpContext) withKubeConfig(rc *rest.Config) *clientcmdapi.Config {
222222
return fakeConfig
223223
}
224224

225-
// withEnvTest sets up the environment for kubeconfig to be used with envTest
226-
func (c *mcpContext) withEnvTest() {
227-
c.withKubeConfig(envTestRestConfig)
228-
}
229-
230-
// inOpenShift sets up the kubernetes environment to seem to be running OpenShift
231-
func inOpenShift(c *mcpContext) {
232-
c.withEnvTest()
233-
crdTemplate := `
234-
{
235-
"apiVersion": "apiextensions.k8s.io/v1",
236-
"kind": "CustomResourceDefinition",
237-
"metadata": {"name": "%s"},
238-
"spec": {
239-
"group": "%s",
240-
"versions": [{
241-
"name": "v1","served": true,"storage": true,
242-
"schema": {"openAPIV3Schema": {"type": "object","x-kubernetes-preserve-unknown-fields": true}}
243-
}],
244-
"scope": "%s",
245-
"names": {"plural": "%s","singular": "%s","kind": "%s"}
246-
}
247-
}`
248-
tasks, _ := errgroup.WithContext(c.ctx)
249-
tasks.Go(func() error {
250-
return c.crdApply(fmt.Sprintf(crdTemplate, "projects.project.openshift.io", "project.openshift.io",
251-
"Cluster", "projects", "project", "Project"))
252-
})
253-
tasks.Go(func() error {
254-
return c.crdApply(fmt.Sprintf(crdTemplate, "routes.route.openshift.io", "route.openshift.io",
255-
"Namespaced", "routes", "route", "Route"))
256-
})
257-
if err := tasks.Wait(); err != nil {
258-
panic(err)
259-
}
260-
}
261-
262-
// inOpenShiftClear clears the kubernetes environment so it no longer seems to be running OpenShift
263-
func inOpenShiftClear(c *mcpContext) {
264-
tasks, _ := errgroup.WithContext(c.ctx)
265-
tasks.Go(func() error { return c.crdDelete("projects.project.openshift.io") })
266-
tasks.Go(func() error { return c.crdDelete("routes.route.openshift.io") })
267-
if err := tasks.Wait(); err != nil {
268-
panic(err)
269-
}
270-
}
271-
272-
// newKubernetesClient creates a new Kubernetes client with the envTest kubeconfig
273-
func (c *mcpContext) newKubernetesClient() *kubernetes.Clientset {
274-
return kubernetes.NewForConfigOrDie(envTestRestConfig)
275-
}
276-
277-
// newApiExtensionsClient creates a new ApiExtensions client with the envTest kubeconfig
278-
func (c *mcpContext) newApiExtensionsClient() *apiextensionsv1.ApiextensionsV1Client {
279-
return apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
280-
}
281-
282-
// crdApply creates a CRD from the provided resource string and waits for it to be established
283-
func (c *mcpContext) crdApply(resource string) error {
284-
apiExtensionsV1Client := c.newApiExtensionsClient()
285-
var crd = &apiextensionsv1spec.CustomResourceDefinition{}
286-
err := json.Unmarshal([]byte(resource), crd)
287-
if err != nil {
288-
return fmt.Errorf("failed to create CRD %v", err)
289-
}
290-
_, err = apiExtensionsV1Client.CustomResourceDefinitions().Create(c.ctx, crd, metav1.CreateOptions{})
291-
if err != nil {
292-
return fmt.Errorf("failed to create CRD %v", err)
293-
}
294-
c.crdWaitUntilReady(crd.Name)
295-
return nil
296-
}
297-
298-
// crdDelete deletes a CRD by name and waits for it to be removed
299-
func (c *mcpContext) crdDelete(name string) error {
300-
apiExtensionsV1Client := c.newApiExtensionsClient()
301-
err := apiExtensionsV1Client.CustomResourceDefinitions().Delete(c.ctx, name, metav1.DeleteOptions{
302-
GracePeriodSeconds: ptr.To(int64(0)),
303-
})
304-
iteration := 0
305-
for iteration < 100 {
306-
if _, derr := apiExtensionsV1Client.CustomResourceDefinitions().Get(c.ctx, name, metav1.GetOptions{}); derr != nil {
307-
break
308-
}
309-
time.Sleep(5 * time.Millisecond)
310-
iteration++
311-
}
312-
if err != nil {
313-
return errors.Wrap(err, "failed to delete CRD")
314-
}
315-
return nil
316-
}
317-
318-
// crdWaitUntilReady waits for a CRD to be established
319-
func (c *mcpContext) crdWaitUntilReady(name string) {
320-
watcher, err := c.newApiExtensionsClient().CustomResourceDefinitions().Watch(c.ctx, metav1.ListOptions{
321-
FieldSelector: "metadata.name=" + name,
322-
})
323-
if err != nil {
324-
panic(fmt.Errorf("failed to watch CRD %v", err))
325-
}
326-
_, err = toolswatch.UntilWithoutRetry(c.ctx, watcher, func(event watch.Event) (bool, error) {
327-
for _, c := range event.Object.(*apiextensionsv1spec.CustomResourceDefinition).Status.Conditions {
328-
if c.Type == apiextensionsv1spec.Established && c.Status == apiextensionsv1spec.ConditionTrue {
329-
return true, nil
330-
}
331-
}
332-
return false, nil
333-
})
334-
if err != nil {
335-
panic(fmt.Errorf("failed to wait for CRD %v", err))
336-
}
337-
}
338-
339225
// callTool helper function to call a tool by name with arguments
340226
func (c *mcpContext) callTool(name string, args map[string]interface{}) (*mcp.CallToolResult, error) {
341227
callToolRequest := mcp.CallToolRequest{}
@@ -446,20 +332,97 @@ func (s *BaseMcpSuite) InitMcpClient(options ...transport.StreamableHTTPCOption)
446332
s.McpClient = test.NewMcpClient(s.T(), s.mcpServer.ServeHTTP(nil), options...)
447333
}
448334

449-
// CrdWaitUntilReady waits for a CRD to be established
450-
func (s *BaseMcpSuite) CrdWaitUntilReady(name string) {
335+
// EnvTestInOpenShift sets up the kubernetes environment to seem to be running OpenShift
336+
func EnvTestInOpenShift(ctx context.Context) error {
337+
crdTemplate := `
338+
{
339+
"apiVersion": "apiextensions.k8s.io/v1",
340+
"kind": "CustomResourceDefinition",
341+
"metadata": {"name": "%s"},
342+
"spec": {
343+
"group": "%s",
344+
"versions": [{
345+
"name": "v1","served": true,"storage": true,
346+
"schema": {"openAPIV3Schema": {"type": "object","x-kubernetes-preserve-unknown-fields": true}}
347+
}],
348+
"scope": "%s",
349+
"names": {"plural": "%s","singular": "%s","kind": "%s"}
350+
}
351+
}`
352+
tasks, _ := errgroup.WithContext(ctx)
353+
tasks.Go(func() error {
354+
return EnvTestCrdApply(ctx, fmt.Sprintf(crdTemplate, "projects.project.openshift.io", "project.openshift.io",
355+
"Cluster", "projects", "project", "Project"))
356+
})
357+
tasks.Go(func() error {
358+
return EnvTestCrdApply(ctx, fmt.Sprintf(crdTemplate, "routes.route.openshift.io", "route.openshift.io",
359+
"Namespaced", "routes", "route", "Route"))
360+
})
361+
return tasks.Wait()
362+
}
363+
364+
// EnvTestInOpenShiftClear clears the kubernetes environment so it no longer seems to be running OpenShift
365+
func EnvTestInOpenShiftClear(ctx context.Context) error {
366+
tasks, _ := errgroup.WithContext(ctx)
367+
tasks.Go(func() error { return EnvTestCrdDelete(ctx, "projects.project.openshift.io") })
368+
tasks.Go(func() error { return EnvTestCrdDelete(ctx, "routes.route.openshift.io") })
369+
return tasks.Wait()
370+
}
371+
372+
// EnvTestCrdWaitUntilReady waits for a CRD to be established
373+
func EnvTestCrdWaitUntilReady(ctx context.Context, name string) error {
451374
apiExtensionClient := apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
452-
watcher, err := apiExtensionClient.CustomResourceDefinitions().Watch(s.T().Context(), metav1.ListOptions{
375+
watcher, err := apiExtensionClient.CustomResourceDefinitions().Watch(ctx, metav1.ListOptions{
453376
FieldSelector: "metadata.name=" + name,
454377
})
455-
s.Require().NoError(err, "failed to watch CRD")
456-
_, err = toolswatch.UntilWithoutRetry(s.T().Context(), watcher, func(event watch.Event) (bool, error) {
378+
if err != nil {
379+
return fmt.Errorf("unable to watch CRDs: %w", err)
380+
}
381+
_, err = toolswatch.UntilWithoutRetry(ctx, watcher, func(event watch.Event) (bool, error) {
457382
for _, c := range event.Object.(*apiextensionsv1spec.CustomResourceDefinition).Status.Conditions {
458383
if c.Type == apiextensionsv1spec.Established && c.Status == apiextensionsv1spec.ConditionTrue {
459384
return true, nil
460385
}
461386
}
462387
return false, nil
463388
})
464-
s.Require().NoError(err, "failed to wait for CRD")
389+
if err != nil {
390+
return fmt.Errorf("failed to wait for CRD: %w", err)
391+
}
392+
return nil
393+
}
394+
395+
// EnvTestCrdApply creates a CRD from the provided resource string and waits for it to be established
396+
func EnvTestCrdApply(ctx context.Context, resource string) error {
397+
apiExtensionsV1Client := apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
398+
var crd = &apiextensionsv1spec.CustomResourceDefinition{}
399+
err := json.Unmarshal([]byte(resource), crd)
400+
if err != nil {
401+
return fmt.Errorf("failed to create CRD %v", err)
402+
}
403+
_, err = apiExtensionsV1Client.CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{})
404+
if err != nil {
405+
return fmt.Errorf("failed to create CRD %v", err)
406+
}
407+
return EnvTestCrdWaitUntilReady(ctx, crd.Name)
408+
}
409+
410+
// crdDelete deletes a CRD by name and waits for it to be removed
411+
func EnvTestCrdDelete(ctx context.Context, name string) error {
412+
apiExtensionsV1Client := apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
413+
err := apiExtensionsV1Client.CustomResourceDefinitions().Delete(ctx, name, metav1.DeleteOptions{
414+
GracePeriodSeconds: ptr.To(int64(0)),
415+
})
416+
iteration := 0
417+
for iteration < 100 {
418+
if _, derr := apiExtensionsV1Client.CustomResourceDefinitions().Get(ctx, name, metav1.GetOptions{}); derr != nil {
419+
break
420+
}
421+
time.Sleep(5 * time.Millisecond)
422+
iteration++
423+
}
424+
if err != nil {
425+
return errors.Wrap(err, "failed to delete CRD")
426+
}
427+
return nil
465428
}

pkg/mcp/namespaces_test.go

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ import (
1313
"k8s.io/apimachinery/pkg/runtime/schema"
1414
"k8s.io/client-go/dynamic"
1515
"sigs.k8s.io/yaml"
16-
17-
"github.com/containers/kubernetes-mcp-server/internal/test"
18-
"github.com/containers/kubernetes-mcp-server/pkg/config"
1916
)
2017

2118
type NamespacesSuite struct {
@@ -108,68 +105,67 @@ func (s *NamespacesSuite) TestNamespacesListAsTable() {
108105
})
109106
}
110107

111-
func TestNamespaces(t *testing.T) {
112-
suite.Run(t, new(NamespacesSuite))
113-
}
108+
func (s *NamespacesSuite) TestProjectsListInOpenShift() {
109+
s.Require().NoError(EnvTestInOpenShift(s.T().Context()), "Expected to configure test for OpenShift")
110+
s.T().Cleanup(func() {
111+
s.Require().NoError(EnvTestInOpenShiftClear(s.T().Context()), "Expected to clear OpenShift test configuration")
112+
})
113+
s.InitMcpClient()
114114

115-
func TestProjectsListInOpenShift(t *testing.T) {
116-
testCaseWithContext(t, &mcpContext{before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) {
115+
s.Run("projects_list returns project list in OpenShift", func() {
117116
dynamicClient := dynamic.NewForConfigOrDie(envTestRestConfig)
118117
_, _ = dynamicClient.Resource(schema.GroupVersionResource{Group: "project.openshift.io", Version: "v1", Resource: "projects"}).
119-
Create(c.ctx, &unstructured.Unstructured{Object: map[string]interface{}{
118+
Create(s.T().Context(), &unstructured.Unstructured{Object: map[string]interface{}{
120119
"apiVersion": "project.openshift.io/v1",
121120
"kind": "Project",
122121
"metadata": map[string]interface{}{
123122
"name": "an-openshift-project",
124123
},
125124
}}, metav1.CreateOptions{})
126-
toolResult, err := c.callTool("projects_list", map[string]interface{}{})
127-
t.Run("projects_list returns project list", func(t *testing.T) {
128-
if err != nil {
129-
t.Fatalf("call tool failed %v", err)
130-
}
131-
if toolResult.IsError {
132-
t.Fatalf("call tool failed")
133-
}
125+
toolResult, err := s.CallTool("projects_list", map[string]interface{}{})
126+
s.Run("no error", func() {
127+
s.Nilf(err, "call tool failed %v", err)
128+
s.Falsef(toolResult.IsError, "call tool failed")
134129
})
135130
var decoded []unstructured.Unstructured
136131
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded)
137-
t.Run("projects_list has yaml content", func(t *testing.T) {
138-
if err != nil {
139-
t.Fatalf("invalid tool result content %v", err)
140-
}
132+
s.Run("has yaml content", func() {
133+
s.Nilf(err, "invalid tool result content %v", err)
141134
})
142-
t.Run("projects_list returns at least 1 items", func(t *testing.T) {
143-
if len(decoded) < 1 {
144-
t.Errorf("invalid project count, expected at least 1, got %v", len(decoded))
145-
}
135+
s.Run("returns at least 1 item", func() {
136+
s.GreaterOrEqualf(len(decoded), 1, "invalid project count, expected at least 1, got %v", len(decoded))
146137
idx := slices.IndexFunc(decoded, func(ns unstructured.Unstructured) bool {
147138
return ns.GetName() == "an-openshift-project"
148139
})
149-
if idx == -1 {
150-
t.Errorf("namespace %s not found in the list", "an-openshift-project")
151-
}
140+
s.NotEqualf(-1, idx, "namespace %s not found in the list", "an-openshift-project")
152141
})
153142
})
154143
}
155144

156-
func TestProjectsListInOpenShiftDenied(t *testing.T) {
157-
deniedResourcesServer := test.Must(config.ReadToml([]byte(`
145+
func (s *NamespacesSuite) TestProjectsListInOpenShiftDenied() {
146+
s.Require().NoError(toml.Unmarshal([]byte(`
158147
denied_resources = [ { group = "project.openshift.io", version = "v1" } ]
159-
`)))
160-
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer, before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) {
161-
c.withEnvTest()
162-
projectsList, _ := c.callTool("projects_list", map[string]interface{}{})
163-
t.Run("projects_list has error", func(t *testing.T) {
164-
if !projectsList.IsError {
165-
t.Fatalf("call tool should fail")
166-
}
148+
`), s.Cfg), "Expected to parse denied resources config")
149+
s.Require().NoError(EnvTestInOpenShift(s.T().Context()), "Expected to configure test for OpenShift")
150+
s.T().Cleanup(func() {
151+
s.Require().NoError(EnvTestInOpenShiftClear(s.T().Context()), "Expected to clear OpenShift test configuration")
152+
})
153+
s.InitMcpClient()
154+
155+
s.Run("projects_list (denied)", func() {
156+
projectsList, err := s.CallTool("projects_list", map[string]interface{}{})
157+
s.Run("has error", func() {
158+
s.Truef(projectsList.IsError, "call tool should fail")
159+
s.Nilf(err, "call tool should not return error object")
167160
})
168-
t.Run("projects_list describes denial", func(t *testing.T) {
161+
s.Run("describes denial", func() {
169162
expectedMessage := "failed to list projects: resource not allowed: project.openshift.io/v1, Kind=Project"
170-
if projectsList.Content[0].(mcp.TextContent).Text != expectedMessage {
171-
t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, projectsList.Content[0].(mcp.TextContent).Text)
172-
}
163+
s.Equalf(expectedMessage, projectsList.Content[0].(mcp.TextContent).Text,
164+
"expected descriptive error '%s', got %v", expectedMessage, projectsList.Content[0].(mcp.TextContent).Text)
173165
})
174166
})
175167
}
168+
169+
func TestNamespaces(t *testing.T) {
170+
suite.Run(t, new(NamespacesSuite))
171+
}

0 commit comments

Comments
 (0)