diff --git a/AGENTS.md b/AGENTS.md index 9a2d41ce..36d658ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,6 +113,134 @@ The test suite relies on the `setup-envtest` tooling from `sigs.k8s.io/controlle The first run downloads a Kubernetes `envtest` environment from the internet, so network access is required. Without it some tests will fail during setup. +### Testing Patterns and Guidelines + +This project follows specific testing patterns to ensure consistency, maintainability, and quality. When writing tests, adhere to the following guidelines: + +#### Test Framework + +- **Use `testify/suite`** for organizing tests into suites +- Tests should be structured using test suites that embed `suite.Suite` +- Each test file should have a corresponding suite struct (e.g., `UnstructuredSuite`, `KubevirtSuite`) +- Use the `suite.Run()` function to execute test suites + +Example: +```go +type MyTestSuite struct { + suite.Suite +} + +func (s *MyTestSuite) TestSomething() { + s.Run("descriptive scenario name", func() { + // test implementation + }) +} + +func TestMyFeature(t *testing.T) { + suite.Run(t, new(MyTestSuite)) +} +``` + +#### Behavior-Based Testing + +- **Test the public API only** - tests should be black-box and not access internal/private functions +- **No mocks** - use real implementations and integration testing where possible +- **Behavior over implementation** - test what the code does, not how it does it +- Focus on observable behavior and outcomes rather than internal state + +#### Test Organization + +- **Use nested subtests** with `s.Run()` to organize related test cases +- **Descriptive names** - subtest names should clearly describe the scenario being tested +- Group related scenarios together under a parent test (e.g., "edge cases", "with valid input") + +Example structure: +```go +func (s *MySuite) TestFeature() { + s.Run("valid input scenarios", func() { + s.Run("handles simple case correctly", func() { + // test code + }) + s.Run("handles complex case with nested data", func() { + // test code + }) + }) + s.Run("edge cases", func() { + s.Run("returns error for nil input", func() { + // test code + }) + s.Run("handles empty input gracefully", func() { + // test code + }) + }) +} +``` + +#### Assertions + +- **One assertion per test case** - each `s.Run()` block should ideally test one specific behavior +- Use `testify` assertion methods: `s.Equal()`, `s.True()`, `s.False()`, `s.Nil()`, `s.NotNil()`, etc. +- Provide clear assertion messages when the failure reason might not be obvious + +Example: +```go +s.Run("returns expected value", func() { + result := functionUnderTest() + s.Equal("expected", result, "function should return the expected string") +}) +``` + +#### Coverage + +- **Aim for high test coverage** of the public API +- Add edge case tests to cover error paths and boundary conditions +- Common edge cases to consider: + - Nil/null inputs + - Empty strings, slices, maps + - Negative numbers or invalid indices + - Type mismatches + - Malformed input (e.g., invalid paths, formats) + +#### Error Handling + +- **Never ignore errors** in production code +- Always check and handle errors from functions that return them +- In tests, use `s.Require().NoError(err)` for operations that must succeed for the test to be valid +- Use `s.Error(err)` or `s.NoError(err)` for testing error conditions + +Example: +```go +s.Run("returns error for invalid input", func() { + result, err := functionUnderTest(invalidInput) + s.Error(err, "expected error for invalid input") + s.Nil(result, "result should be nil when error occurs") +}) +``` + +#### Test Helpers + +- Create reusable test helpers in `internal/test/` for common testing utilities +- Test helpers should be generic and reusable across multiple test files +- Document test helpers with clear godoc comments explaining their purpose and usage + +Example from this project: +```go +// FieldString retrieves a string field from an unstructured object using JSONPath-like notation. +// Examples: +// - "spec.runStrategy" +// - "spec.template.spec.volumes[0].containerDisk.image" +func FieldString(obj *unstructured.Unstructured, path string) string { + // implementation +} +``` + +#### Examples from the Codebase + +Good examples of these patterns can be found in: +- `internal/test/unstructured_test.go` - demonstrates proper use of testify/suite, nested subtests, and edge case testing +- `pkg/mcp/kubevirt_test.go` - shows behavior-based testing of the MCP layer +- `pkg/kubernetes/manager_test.go` - illustrates testing with proper setup/teardown and subtests + ## Linting Static analysis is performed with `golangci-lint`: diff --git a/internal/test/unstructured.go b/internal/test/unstructured.go new file mode 100644 index 00000000..1b4f89e6 --- /dev/null +++ b/internal/test/unstructured.go @@ -0,0 +1,172 @@ +// Package test provides utilities for testing unstructured Kubernetes objects. +// +// The primary functionality is JSONPath-like field access for unstructured.Unstructured objects, +// making test assertions more readable and maintainable. +package test + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// FieldString retrieves a string field from an unstructured object using JSONPath-like notation. +// Examples: +// - "spec.runStrategy" +// - "spec.template.spec.volumes[0].containerDisk.image" +// - "spec.dataVolumeTemplates[0].spec.sourceRef.kind" +func FieldString(obj *unstructured.Unstructured, path string) string { + if obj == nil { + return "" + } + value, _ := Field(obj.Object, path) + if str, ok := value.(string); ok { + return str + } + return "" +} + +// FieldExists checks if a field exists at the given JSONPath-like path. +func FieldExists(obj *unstructured.Unstructured, path string) bool { + if obj == nil { + return false + } + _, found := Field(obj.Object, path) + return found +} + +// FieldInt retrieves an integer field from an unstructured object using JSONPath-like notation. +// Returns 0 if the field is not found or is not an integer type (int, int64, int32). +// Examples: +// - "spec.replicas" +// - "spec.ports[0].containerPort" +func FieldInt(obj *unstructured.Unstructured, path string) int64 { + if obj == nil { + return 0 + } + value, _ := Field(obj.Object, path) + switch v := value.(type) { + case int64: + return v + case int: + return int64(v) + case int32: + return int64(v) + default: + return 0 + } +} + +// FieldValue retrieves any field value from an unstructured object using JSONPath-like notation. +// Returns nil if the field is not found. This is useful when you need the raw value +// without type conversion. +// Examples: +// - "spec.template.spec.containers[0]" - returns map[string]interface{} +// - "metadata.labels" - returns map[string]interface{} +func FieldValue(obj *unstructured.Unstructured, path string) interface{} { + if obj == nil { + return nil + } + value, _ := Field(obj.Object, path) + return value +} + +// Field is the core helper that traverses an unstructured object using JSONPath-like notation. +// It supports both dot notation (foo.bar) and array indexing (foo[0].bar). +func Field(obj interface{}, path string) (interface{}, bool) { + if obj == nil || path == "" { + return nil, false + } + + // Parse the path into segments + segments := parsePath(path) + current := obj + + for _, segment := range segments { + if segment.isArray { + // Handle array indexing + slice, ok := current.([]interface{}) + if !ok { + return nil, false + } + if segment.index >= len(slice) || segment.index < 0 { + return nil, false + } + current = slice[segment.index] + } else { + // Handle map field access + m, ok := current.(map[string]interface{}) + if !ok { + return nil, false + } + val, exists := m[segment.field] + if !exists { + return nil, false + } + current = val + } + } + + return current, true +} + +type pathSegment struct { + field string + isArray bool + index int +} + +// parsePath converts a JSONPath-like string into segments. +// Examples: +// - "spec.runStrategy" -> [{field: "spec"}, {field: "runStrategy"}] +// - "spec.volumes[0].name" -> [{field: "spec"}, {field: "volumes"}, {isArray: true, index: 0}, {field: "name"}] +func parsePath(path string) []pathSegment { + var segments []pathSegment + current := "" + inBracket := false + indexStr := "" + + for i := 0; i < len(path); i++ { + ch := path[i] + switch ch { + case '.': + if inBracket { + indexStr += string(ch) + } else if current != "" { + segments = append(segments, pathSegment{field: current}) + current = "" + } + case '[': + if current != "" { + segments = append(segments, pathSegment{field: current}) + current = "" + } + inBracket = true + indexStr = "" + case ']': + if inBracket { + // Parse the index + var idx int + if _, err := fmt.Sscanf(indexStr, "%d", &idx); err != nil { + // If parsing fails, use -1 as invalid index + idx = -1 + } + segments = append(segments, pathSegment{isArray: true, index: idx}) + inBracket = false + indexStr = "" + } + default: + if inBracket { + indexStr += string(ch) + } else { + current += string(ch) + } + } + } + + if current != "" { + segments = append(segments, pathSegment{field: current}) + } + + return segments +} diff --git a/internal/test/unstructured_test.go b/internal/test/unstructured_test.go new file mode 100644 index 00000000..a228300a --- /dev/null +++ b/internal/test/unstructured_test.go @@ -0,0 +1,376 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type UnstructuredSuite struct { + suite.Suite +} + +func (s *UnstructuredSuite) TestFieldString() { + s.Run("simple field access", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + }, + }, + } + s.Run("returns field value", func() { + s.Equal("Halted", FieldString(obj, "spec.runStrategy")) + }) + }) + + s.Run("nested field access", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "instancetype": map[string]interface{}{ + "kind": "VirtualMachineClusterInstancetype", + "name": "u1.medium", + }, + }, + }, + } + s.Run("returns nested field value", func() { + s.Equal("VirtualMachineClusterInstancetype", FieldString(obj, "spec.instancetype.kind")) + }) + s.Run("returns deeply nested field value", func() { + s.Equal("u1.medium", FieldString(obj, "spec.instancetype.name")) + }) + }) + + s.Run("array indexing", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "volumes": []interface{}{ + map[string]interface{}{ + "name": "vol1", + "containerDisk": map[string]interface{}{ + "image": "quay.io/containerdisks/fedora:latest", + }, + }, + }, + }, + }, + }, + }, + } + s.Run("returns field from first array element", func() { + s.Equal("vol1", FieldString(obj, "spec.template.spec.volumes[0].name")) + }) + s.Run("returns nested field from array element", func() { + s.Equal("quay.io/containerdisks/fedora:latest", FieldString(obj, "spec.template.spec.volumes[0].containerDisk.image")) + }) + }) + + s.Run("non-existent fields", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + }, + }, + } + s.Run("returns empty string for non-existent field", func() { + s.Equal("", FieldString(obj, "spec.nonexistent")) + }) + s.Run("returns empty string for invalid array index", func() { + s.Equal("", FieldString(obj, "spec.volumes[999].name")) + }) + }) + + s.Run("edge cases", func() { + s.Run("returns empty string for empty path", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + }, + }, + } + s.Equal("", FieldString(obj, "")) + }) + s.Run("returns empty string for nil object", func() { + s.Equal("", FieldString(nil, "spec.runStrategy")) + }) + s.Run("returns empty string when trying to index non-array", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + }, + }, + } + s.Equal("", FieldString(obj, "spec.runStrategy[0]")) + }) + s.Run("returns empty string when trying to access field on non-map", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "volumes": []interface{}{ + "simple-string", + }, + }, + }, + } + s.Equal("", FieldString(obj, "spec.volumes[0].name")) + }) + s.Run("returns empty string for negative array index", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "volumes": []interface{}{ + map[string]interface{}{"name": "vol1"}, + }, + }, + }, + } + s.Equal("", FieldString(obj, "spec.volumes[-1].name")) + }) + s.Run("handles path with consecutive dots gracefully", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + }, + }, + } + // Parser skips empty segments, so consecutive dots are ignored + s.Equal("Halted", FieldString(obj, "spec..runStrategy")) + }) + s.Run("handles path starting with dot gracefully", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + }, + }, + } + // Parser skips empty segments at the start + s.Equal("Halted", FieldString(obj, ".spec.runStrategy")) + }) + s.Run("handles path ending with dot", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + }, + }, + } + s.Equal("Halted", FieldString(obj, "spec.runStrategy.")) + }) + s.Run("handles dot inside brackets gracefully", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "volumes": []interface{}{ + map[string]interface{}{"name": "vol1"}, + }, + }, + }, + } + // Parser treats "0.5" as the index string, fmt.Sscanf will parse it as 0 + s.Equal("vol1", FieldString(obj, "spec.volumes[0.5].name")) + }) + s.Run("returns empty string for non-numeric array index", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "volumes": []interface{}{ + map[string]interface{}{"name": "vol1"}, + }, + }, + }, + } + // Parser fails to parse "abc" as integer, defaults to -1 which is invalid + s.Equal("", FieldString(obj, "spec.volumes[abc].name")) + }) + }) +} + +func (s *UnstructuredSuite) TestFieldExists() { + s.Run("existing fields", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + "instancetype": map[string]interface{}{ + "kind": "VirtualMachineClusterInstancetype", + }, + }, + }, + } + s.Run("returns true for simple field", func() { + s.True(FieldExists(obj, "spec.runStrategy")) + }) + s.Run("returns true for nested object", func() { + s.True(FieldExists(obj, "spec.instancetype")) + }) + s.Run("returns true for nested field", func() { + s.True(FieldExists(obj, "spec.instancetype.kind")) + }) + }) + + s.Run("non-existent fields", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + "instancetype": map[string]interface{}{ + "kind": "VirtualMachineClusterInstancetype", + }, + }, + }, + } + s.Run("returns false for non-existent simple field", func() { + s.False(FieldExists(obj, "spec.preference")) + }) + s.Run("returns false for non-existent nested field", func() { + s.False(FieldExists(obj, "spec.instancetype.name")) + }) + }) + + s.Run("edge cases", func() { + s.Run("returns false for empty path", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "runStrategy": "Halted", + }, + }, + } + s.False(FieldExists(obj, "")) + }) + s.Run("returns false for nil object", func() { + s.False(FieldExists(nil, "spec.runStrategy")) + }) + }) +} + +func (s *UnstructuredSuite) TestFieldInt() { + s.Run("integer field access", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "replicas": int64(3), + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(80), + }, + }, + }, + }, + } + s.Run("returns int64 field value", func() { + s.Equal(int64(3), FieldInt(obj, "spec.replicas")) + }) + s.Run("returns int64 from array element", func() { + s.Equal(int64(80), FieldInt(obj, "spec.ports[0].containerPort")) + }) + }) + + s.Run("different integer types", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "int64Value": int64(100), + "intValue": int(200), + "int32Value": int32(300), + }, + }, + } + s.Run("handles int64 type", func() { + s.Equal(int64(100), FieldInt(obj, "spec.int64Value")) + }) + s.Run("handles int type", func() { + s.Equal(int64(200), FieldInt(obj, "spec.intValue")) + }) + s.Run("handles int32 type", func() { + s.Equal(int64(300), FieldInt(obj, "spec.int32Value")) + }) + }) + + s.Run("edge cases", func() { + s.Run("returns 0 for non-existent field", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "replicas": int64(3), + }, + }, + } + s.Equal(int64(0), FieldInt(obj, "spec.nonexistent")) + }) + s.Run("returns 0 for nil object", func() { + s.Equal(int64(0), FieldInt(nil, "spec.replicas")) + }) + s.Run("returns 0 for non-integer field", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "name": "test-string", + }, + }, + } + s.Equal(int64(0), FieldInt(obj, "spec.name")) + }) + }) +} + +func (s *UnstructuredSuite) TestFieldValue() { + s.Run("returns any field value", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "test", + }, + }, + "spec": map[string]interface{}{ + "replicas": int64(3), + "name": "test-name", + }, + }, + } + s.Run("returns map value", func() { + labels := FieldValue(obj, "metadata.labels") + labelsMap, ok := labels.(map[string]interface{}) + s.True(ok, "expected map[string]interface{}") + s.Equal("test", labelsMap["app"]) + }) + s.Run("returns int64 value", func() { + replicas := FieldValue(obj, "spec.replicas") + s.Equal(int64(3), replicas) + }) + s.Run("returns string value", func() { + name := FieldValue(obj, "spec.name") + s.Equal("test-name", name) + }) + }) + + s.Run("edge cases", func() { + s.Run("returns nil for non-existent field", func() { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{}, + }, + } + s.Nil(FieldValue(obj, "spec.nonexistent")) + }) + s.Run("returns nil for nil object", func() { + s.Nil(FieldValue(nil, "spec.name")) + }) + }) +} + +func TestUnstructured(t *testing.T) { + suite.Run(t, new(UnstructuredSuite)) +} diff --git a/pkg/mcp/kubevirt_test.go b/pkg/mcp/kubevirt_test.go index 9a09f521..83e6e9ed 100644 --- a/pkg/mcp/kubevirt_test.go +++ b/pkg/mcp/kubevirt_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/BurntSushi/toml" + "github.com/containers/kubernetes-mcp-server/internal/test" kubevirttesting "github.com/containers/kubernetes-mcp-server/pkg/kubevirt/testing" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/suite" @@ -96,15 +97,12 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("quay.io/containerdisks/fedora:latest", - decodedResult[0].Object["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["volumes"].([]interface{})[0].(map[string]interface{})["containerDisk"].(map[string]interface{})["image"].(string), - "invalid default image") - s.Equal("Halted", - decodedResult[0].Object["spec"].(map[string]interface{})["runStrategy"].(string), - "invalid default runStrategy") + vm := &decodedResult[0] + s.Equal("test-vm", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("quay.io/containerdisks/fedora:latest", test.FieldString(vm, "spec.template.spec.volumes[0].containerDisk.image"), "invalid default image") + s.Equal("Halted", test.FieldString(vm, "spec.runStrategy"), "invalid default runStrategy") }) }) s.Run("vm_create(workload=ubuntu, instancetype=u1.medium) with instancetype", func() { @@ -125,21 +123,14 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-2", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("quay.io/containerdisks/ubuntu:24.04", - decodedResult[0].Object["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["volumes"].([]interface{})[0].(map[string]interface{})["containerDisk"].(map[string]interface{})["image"].(string), - "invalid image for ubuntu workload") - s.Equal("Halted", - decodedResult[0].Object["spec"].(map[string]interface{})["runStrategy"].(string), - "invalid default runStrategy") - s.Equal("VirtualMachineClusterInstancetype", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["kind"].(string), - "invalid memory for u1.medium instanceType") - s.Equal("u1.medium", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["name"].(string), - "invalid cpu cores for u1.medium instanceType") + vm := &decodedResult[0] + s.Equal("test-vm-2", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("quay.io/containerdisks/ubuntu:24.04", test.FieldString(vm, "spec.template.spec.volumes[0].containerDisk.image"), "invalid image for ubuntu workload") + s.Equal("Halted", test.FieldString(vm, "spec.runStrategy"), "invalid default runStrategy") + s.Equal("VirtualMachineClusterInstancetype", test.FieldString(vm, "spec.instancetype.kind"), "invalid memory for u1.medium instanceType") + s.Equal("u1.medium", test.FieldString(vm, "spec.instancetype.name"), "invalid cpu cores for u1.medium instanceType") }) }) s.Run("vm_create(workload=rhel, preference=rhel.9) with preference", func() { @@ -160,21 +151,14 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-3", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("rhel", - decodedResult[0].Object["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["volumes"].([]interface{})[0].(map[string]interface{})["containerDisk"].(map[string]interface{})["image"].(string), - "invalid image for rhel workload") - s.Equal("Halted", - decodedResult[0].Object["spec"].(map[string]interface{})["runStrategy"].(string), - "invalid default runStrategy") - s.Equal("VirtualMachineClusterPreference", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["kind"].(string), - "invalid preference kind for rhel.9 preference") - s.Equal("rhel.9", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["name"].(string), - "invalid preference name for rhel.9 preference") + vm := &decodedResult[0] + s.Equal("test-vm-3", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("rhel", test.FieldString(vm, "spec.template.spec.volumes[0].containerDisk.image"), "invalid image for rhel workload") + s.Equal("Halted", test.FieldString(vm, "spec.runStrategy"), "invalid default runStrategy") + s.Equal("VirtualMachineClusterPreference", test.FieldString(vm, "spec.preference.kind"), "invalid preference kind for rhel.9 preference") + s.Equal("rhel.9", test.FieldString(vm, "spec.preference.name"), "invalid preference name for rhel.9 preference") }) }) s.Run("vm_create(workload=quay.io/myrepo/myimage:v1.0) with custom container disk", func() { @@ -194,15 +178,12 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-4", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("quay.io/myrepo/myimage:v1.0", - decodedResult[0].Object["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["volumes"].([]interface{})[0].(map[string]interface{})["containerDisk"].(map[string]interface{})["image"].(string), - "invalid image for custom container disk workload") - s.Equal("Halted", - decodedResult[0].Object["spec"].(map[string]interface{})["runStrategy"].(string), - "invalid default runStrategy") + vm := &decodedResult[0] + s.Equal("test-vm-4", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("quay.io/myrepo/myimage:v1.0", test.FieldString(vm, "spec.template.spec.volumes[0].containerDisk.image"), "invalid image for custom container disk workload") + s.Equal("Halted", test.FieldString(vm, "spec.runStrategy"), "invalid default runStrategy") }) }) s.Run("with size", func() { @@ -244,15 +225,12 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-5", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("VirtualMachineClusterInstancetype", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["kind"].(string), - "invalid instanceType kind for medium size hint") - s.Equal("u1.medium", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["name"].(string), - "invalid instanceType name for medium size hint") + vm := &decodedResult[0] + s.Equal("test-vm-5", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("VirtualMachineClusterInstancetype", test.FieldString(vm, "spec.instancetype.kind"), "invalid instanceType kind for medium size hint") + s.Equal("u1.medium", test.FieldString(vm, "spec.instancetype.name"), "invalid instanceType name for medium size hint") }) }) s.Run("vm_create(size=large, performance=compute-optimized) with size and performance hints", func() { @@ -273,15 +251,12 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-6", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("VirtualMachineClusterInstancetype", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["kind"].(string), - "invalid instanceType kind for large size hint") - s.Equal("c1.large", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["name"].(string), - "invalid instanceType name for large size hint") + vm := &decodedResult[0] + s.Equal("test-vm-6", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("VirtualMachineClusterInstancetype", test.FieldString(vm, "spec.instancetype.kind"), "invalid instanceType kind for large size hint") + s.Equal("c1.large", test.FieldString(vm, "spec.instancetype.name"), "invalid instanceType name for large size hint") }) }) s.Run("vm_create(size=xlarge) with size hint not matching any instancetype", func() { @@ -301,11 +276,11 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-7", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - _, exists := decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"] - s.Falsef(exists, "expected no instancetype to be set for xlarge size hint") + vm := &decodedResult[0] + s.Equal("test-vm-7", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Falsef(test.FieldExists(vm, "spec.instancetype"), "expected no instancetype to be set for xlarge size hint") }) }) }) @@ -337,30 +312,17 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-8", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("Halted", - decodedResult[0].Object["spec"].(map[string]interface{})["runStrategy"].(string), - "invalid default runStrategy") - s.Equal("VirtualMachineClusterInstancetype", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["kind"].(string), - "invalid instanceType kind from DataSource default") - s.Equal("u1.medium", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["name"].(string), - "invalid instanceType name from DataSource default") - s.Equal("VirtualMachineClusterPreference", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["kind"].(string), - "invalid preference kind from DataSource default") - s.Equal("fedora", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["name"].(string), - "invalid preference name from DataSource default") - s.Equal("DataSource", - decodedResult[0].Object["spec"].(map[string]interface{})["dataVolumeTemplates"].([]interface{})[0].(map[string]interface{})["spec"].(map[string]interface{})["sourceRef"].(map[string]interface{})["kind"].(string), - "invalid data source kind in dataVolumeTemplates") - s.Equal("fedora", - decodedResult[0].Object["spec"].(map[string]interface{})["dataVolumeTemplates"].([]interface{})[0].(map[string]interface{})["spec"].(map[string]interface{})["sourceRef"].(map[string]interface{})["name"].(string), - "invalid data source name in dataVolumeTemplates") + vm := &decodedResult[0] + s.Equal("test-vm-8", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("Halted", test.FieldString(vm, "spec.runStrategy"), "invalid default runStrategy") + s.Equal("VirtualMachineClusterInstancetype", test.FieldString(vm, "spec.instancetype.kind"), "invalid instanceType kind from DataSource default") + s.Equal("u1.medium", test.FieldString(vm, "spec.instancetype.name"), "invalid instanceType name from DataSource default") + s.Equal("VirtualMachineClusterPreference", test.FieldString(vm, "spec.preference.kind"), "invalid preference kind from DataSource default") + s.Equal("fedora", test.FieldString(vm, "spec.preference.name"), "invalid preference name from DataSource default") + s.Equal("DataSource", test.FieldString(vm, "spec.dataVolumeTemplates[0].spec.sourceRef.kind"), "invalid data source kind in dataVolumeTemplates") + s.Equal("fedora", test.FieldString(vm, "spec.dataVolumeTemplates[0].spec.sourceRef.name"), "invalid data source name in dataVolumeTemplates") }) }) s.Run("vm_create(workload=rhel) using DataSource partial name match", func() { @@ -387,24 +349,15 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-9", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("Halted", - decodedResult[0].Object["spec"].(map[string]interface{})["runStrategy"].(string), - "invalid default runStrategy") - s.Equal("VirtualMachineClusterPreference", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["kind"].(string), - "invalid preference kind from DataSource default") - s.Equal("rhel.9", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["name"].(string), - "invalid preference name from DataSource default") - s.Equal("DataSource", - decodedResult[0].Object["spec"].(map[string]interface{})["dataVolumeTemplates"].([]interface{})[0].(map[string]interface{})["spec"].(map[string]interface{})["sourceRef"].(map[string]interface{})["kind"].(string), - "invalid data source kind in dataVolumeTemplates") - s.Equal("rhel9", - decodedResult[0].Object["spec"].(map[string]interface{})["dataVolumeTemplates"].([]interface{})[0].(map[string]interface{})["spec"].(map[string]interface{})["sourceRef"].(map[string]interface{})["name"].(string), - "invalid data source name in dataVolumeTemplates") + vm := &decodedResult[0] + s.Equal("test-vm-9", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("Halted", test.FieldString(vm, "spec.runStrategy"), "invalid default runStrategy") + s.Equal("VirtualMachineClusterPreference", test.FieldString(vm, "spec.preference.kind"), "invalid preference kind from DataSource default") + s.Equal("rhel.9", test.FieldString(vm, "spec.preference.name"), "invalid preference name from DataSource default") + s.Equal("DataSource", test.FieldString(vm, "spec.dataVolumeTemplates[0].spec.sourceRef.kind"), "invalid data source kind in dataVolumeTemplates") + s.Equal("rhel9", test.FieldString(vm, "spec.dataVolumeTemplates[0].spec.sourceRef.name"), "invalid data source name in dataVolumeTemplates") }) }) s.Run("vm_create(workload=fedora, size=large) with size hint overriding DataSource default instancetype", func() { @@ -425,18 +378,13 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-10", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("Halted", - decodedResult[0].Object["spec"].(map[string]interface{})["runStrategy"].(string), - "invalid default runStrategy") - s.Equal("VirtualMachineClusterInstancetype", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["kind"].(string), - "invalid instanceType kind for large size hint") - s.Equal("u1.large", - decodedResult[0].Object["spec"].(map[string]interface{})["instancetype"].(map[string]interface{})["name"].(string), - "invalid instanceType name for large size hint") + vm := &decodedResult[0] + s.Equal("test-vm-10", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("Halted", test.FieldString(vm, "spec.runStrategy"), "invalid default runStrategy") + s.Equal("VirtualMachineClusterInstancetype", test.FieldString(vm, "spec.instancetype.kind"), "invalid instanceType kind for large size hint") + s.Equal("u1.large", test.FieldString(vm, "spec.instancetype.name"), "invalid instanceType name for large size hint") }) }) }) @@ -469,15 +417,12 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-11", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("VirtualMachineClusterPreference", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["kind"].(string), - "invalid preference kind for rhel.9 preference") - s.Equal("rhel.9", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["name"].(string), - "invalid preference name for rhel.9 preference") + vm := &decodedResult[0] + s.Equal("test-vm-11", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("VirtualMachineClusterPreference", test.FieldString(vm, "spec.preference.kind"), "invalid preference kind for rhel.9 preference") + s.Equal("rhel.9", test.FieldString(vm, "spec.preference.name"), "invalid preference name for rhel.9 preference") }) }) s.Run("vm_create(workload=fedora, preference=custom.preference) with explicit preference overriding auto-selected preference", func() { @@ -498,12 +443,11 @@ func (s *KubevirtSuite) TestCreate() { s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine created successfully"), "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) - s.Equal("test-vm-12", decodedResult[0].GetName(), "invalid resource name") - s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") - s.NotEmptyf(decodedResult[0].GetUID(), "invalid uid, got %v", decodedResult[0].GetUID()) - s.Equal("custom.preference", - decodedResult[0].Object["spec"].(map[string]interface{})["preference"].(map[string]interface{})["name"].(string), - "invalid preference name for explicit custom.preference") + vm := &decodedResult[0] + s.Equal("test-vm-12", vm.GetName(), "invalid resource name") + s.Equal("default", vm.GetNamespace(), "invalid resource namespace") + s.NotEmptyf(vm.GetUID(), "invalid uid, got %v", vm.GetUID()) + s.Equal("custom.preference", test.FieldString(vm, "spec.preference.name"), "invalid preference name for explicit custom.preference") }) }) }) diff --git a/pkg/mcp/pods_run_test.go b/pkg/mcp/pods_run_test.go index 83370045..f63ee3a1 100644 --- a/pkg/mcp/pods_run_test.go +++ b/pkg/mcp/pods_run_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/BurntSushi/toml" + "github.com/containers/kubernetes-mcp-server/internal/test" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/suite" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -46,15 +47,14 @@ func (s *PodsRunSuite) TestPodsRun() { "invalid pod name, expected random, got %v", decodedNilNamespace[0].GetName()) }) s.Run("returns pod with labels", func() { - labels := decodedNilNamespace[0].Object["metadata"].(map[string]interface{})["labels"].(map[string]interface{}) - s.NotEqualf("", labels["app.kubernetes.io/name"], "invalid labels, expected app.kubernetes.io/name, got %v", labels) - s.NotEqualf("", labels["app.kubernetes.io/component"], "invalid labels, expected app.kubernetes.io/component, got %v", labels) - s.Equalf("kubernetes-mcp-server", labels["app.kubernetes.io/managed-by"], "invalid labels, expected app.kubernetes.io/managed-by, got %v", labels) - s.Equalf("kubernetes-mcp-server-run-sandbox", labels["app.kubernetes.io/part-of"], "invalid labels, expected app.kubernetes.io/part-of, got %v", labels) + labels := test.FieldValue(&decodedNilNamespace[0], "metadata.labels").(map[string]interface{}) + s.Contains(labels["app.kubernetes.io/name"], "kubernetes-mcp-server-run-", "invalid labels, expected app.kubernetes.io/name") + s.Contains(labels["app.kubernetes.io/component"], "kubernetes-mcp-server-run-", "invalid labels, expected app.kubernetes.io/component") + s.Equal("kubernetes-mcp-server", labels["app.kubernetes.io/managed-by"], "invalid labels, expected app.kubernetes.io/managed-by") + s.Equal("kubernetes-mcp-server-run-sandbox", labels["app.kubernetes.io/part-of"], "invalid labels, expected app.kubernetes.io/part-of") }) s.Run("returns pod with nginx container", func() { - containers := decodedNilNamespace[0].Object["spec"].(map[string]interface{})["containers"].([]interface{}) - s.Equalf("nginx", containers[0].(map[string]interface{})["image"], "invalid container name, expected nginx, got %v", containers[0].(map[string]interface{})["image"]) + s.Equal("nginx", test.FieldString(&decodedNilNamespace[0], "spec.containers[0].image"), "invalid container image, expected nginx") }) }) s.Run("pods_run(image=nginx, namespace=nil, port=80)", func() { @@ -74,18 +74,17 @@ func (s *PodsRunSuite) TestPodsRun() { s.Equalf("Service", decodedNamespaceAndPort[1].GetKind(), "invalid service kind, expected Service, got %v", decodedNamespaceAndPort[1].GetKind()) }) s.Run("returns pod with port", func() { - containers := decodedNamespaceAndPort[0].Object["spec"].(map[string]interface{})["containers"].([]interface{}) - ports := containers[0].(map[string]interface{})["ports"].([]interface{}) - s.Equalf(int64(80), ports[0].(map[string]interface{})["containerPort"], "invalid container port, expected 80, got %v", ports[0].(map[string]interface{})["containerPort"]) + pod := &decodedNamespaceAndPort[0] + s.Equal(int64(80), test.FieldInt(pod, "spec.containers[0].ports[0].containerPort"), "invalid container port, expected 80") }) s.Run("returns service with port and selector", func() { - ports := decodedNamespaceAndPort[1].Object["spec"].(map[string]interface{})["ports"].([]interface{}) - s.Equalf(int64(80), ports[0].(map[string]interface{})["port"], "invalid service port, expected 80, got %v", ports[0].(map[string]interface{})["port"]) - s.Equalf(int64(80), ports[0].(map[string]interface{})["targetPort"], "invalid service target port, expected 80, got %v", ports[0].(map[string]interface{})["targetPort"]) - selector := decodedNamespaceAndPort[1].Object["spec"].(map[string]interface{})["selector"].(map[string]interface{}) - s.NotEqualf("", selector["app.kubernetes.io/name"], "invalid service selector, expected app.kubernetes.io/name, got %v", selector) - s.Equalf("kubernetes-mcp-server", selector["app.kubernetes.io/managed-by"], "invalid service selector, expected app.kubernetes.io/managed-by, got %v", selector) - s.Equalf("kubernetes-mcp-server-run-sandbox", selector["app.kubernetes.io/part-of"], "invalid service selector, expected app.kubernetes.io/part-of, got %v", selector) + svc := &decodedNamespaceAndPort[1] + s.Equal(int64(80), test.FieldInt(svc, "spec.ports[0].port"), "invalid service port, expected 80") + s.Equal(int64(80), test.FieldInt(svc, "spec.ports[0].targetPort"), "invalid service target port, expected 80") + selectorMap := test.FieldValue(svc, "spec.selector").(map[string]interface{}) + s.NotEqual("", selectorMap["app.kubernetes.io/name"], "invalid service selector, expected app.kubernetes.io/name") + s.Equal("kubernetes-mcp-server", selectorMap["app.kubernetes.io/managed-by"], "invalid service selector, expected app.kubernetes.io/managed-by") + s.Equal("kubernetes-mcp-server-run-sandbox", selectorMap["app.kubernetes.io/part-of"], "invalid service selector, expected app.kubernetes.io/part-of") }) }) } @@ -136,8 +135,8 @@ func (s *PodsRunSuite) TestPodsRunInOpenShift() { s.Equalf("Route", decodedPodServiceRoute[2].GetKind(), "invalid route kind, expected Route, got %v", decodedPodServiceRoute[2].GetKind()) }) s.Run("returns route with port", func() { - targetPort := decodedPodServiceRoute[2].Object["spec"].(map[string]interface{})["port"].(map[string]interface{})["targetPort"].(int64) - s.Equalf(int64(80), targetPort, "invalid route target port, expected 80, got %v", targetPort) + route := &decodedPodServiceRoute[2] + s.Equal(int64(80), test.FieldInt(route, "spec.port.targetPort"), "invalid route target port, expected 80") }) }) }