From 3080c8e2ef641112893baf5926fcc9e389301cba Mon Sep 17 00:00:00 2001 From: Andy Anderson Date: Wed, 24 Jun 2026 21:40:19 -0400 Subject: [PATCH] =?UTF-8?q?Signed-off-by:=20clubanderson=20\n\n[quality]=20test:=20add=2035=20unit?= =?UTF-8?q?=20tests=20for=20MCP=20custom=20resources=20helpers\n\nAdds=20c?= =?UTF-8?q?omprehensive=20unit=20tests=20for=206=20pure=20helper=20functio?= =?UTF-8?q?ns=20in=20the=20MCP\ncustom=20resources=20handler=20that=20had?= =?UTF-8?q?=20zero=20test=20coverage.=20The=20package=20is\nat=2015.8%=20c?= =?UTF-8?q?overage=20=E2=80=94=20these=20tests=20target=20the=20pagination?= =?UTF-8?q?,=20parsing,=20and\nvalidation=20functions:\n\n-=20parseCustomR?= =?UTF-8?q?esourceLimit:=203=20tests=20(default,=20valid=20values,=20inval?= =?UTF-8?q?id)\n-=20parseCustomResourceContinueToken:=203=20tests=20(empty?= =?UTF-8?q?,=20valid,=20invalid)\n-=20parsePositiveIntQuery:=204=20tests?= =?UTF-8?q?=20(range,=20below=20min,=20above=20max,=20non-int)\n-=20pagina?= =?UTF-8?q?teCustomResourceItems:=206=20tests=20(basic,=20second=20page,?= =?UTF-8?q?=20past=20end,\n=20=20empty,=20exact=20boundary,=20partial=20la?= =?UTF-8?q?st=20page)\n-=20parseCRItem:=204=20tests=20(complete,=20minimal?= =?UTF-8?q?,=20empty,=20non-string=20labels)\n-=20minCustomResourceInt:=20?= =?UTF-8?q?5=20tests=20(all=20comparison=20cases)\n\nCo-authored-by:=20Cop?= =?UTF-8?q?ilot=20<223556219+Copilot@users.noreply.github.com>"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mcp/custom_resources_helpers_test.go | 340 ++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 pkg/api/handlers/mcp/custom_resources_helpers_test.go diff --git a/pkg/api/handlers/mcp/custom_resources_helpers_test.go b/pkg/api/handlers/mcp/custom_resources_helpers_test.go new file mode 100644 index 0000000000..7ce8814246 --- /dev/null +++ b/pkg/api/handlers/mcp/custom_resources_helpers_test.go @@ -0,0 +1,340 @@ +package mcp + +import ( + "fmt" + "testing" +) + +// --- Tests for parseCustomResourceLimit --- + +func TestParseCustomResourceLimit_Default(t *testing.T) { + got, err := parseCustomResourceLimit("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != defaultCustomResourceLimit { + t.Errorf("got %d, want %d", got, defaultCustomResourceLimit) + } +} + +func TestParseCustomResourceLimit_Valid(t *testing.T) { + cases := []struct { + input string + want int + }{ + {"1", 1}, + {"100", 100}, + {"500", 500}, + {"2000", 2000}, + } + for _, tc := range cases { + t.Run(tc.input, func(t *testing.T) { + got, err := parseCustomResourceLimit(tc.input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.want { + t.Errorf("got %d, want %d", got, tc.want) + } + }) + } +} + +func TestParseCustomResourceLimit_Invalid(t *testing.T) { + cases := []string{"0", "-1", "2001", "abc", "3.14"} + for _, tc := range cases { + t.Run(tc, func(t *testing.T) { + _, err := parseCustomResourceLimit(tc) + if err == nil { + t.Error("expected error for invalid limit") + } + }) + } +} + +// --- Tests for parseCustomResourceContinueToken --- + +func TestParseCustomResourceContinueToken_Empty(t *testing.T) { + got, err := parseCustomResourceContinueToken("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != 0 { + t.Errorf("got %d, want 0", got) + } +} + +func TestParseCustomResourceContinueToken_Valid(t *testing.T) { + cases := []struct { + input string + want int + }{ + {"0", 0}, + {"500", 500}, + {"10000", 10000}, + } + for _, tc := range cases { + t.Run(tc.input, func(t *testing.T) { + got, err := parseCustomResourceContinueToken(tc.input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.want { + t.Errorf("got %d, want %d", got, tc.want) + } + }) + } +} + +func TestParseCustomResourceContinueToken_Invalid(t *testing.T) { + cases := []string{"-1", "10001", "abc", "3.14"} + for _, tc := range cases { + t.Run(tc, func(t *testing.T) { + _, err := parseCustomResourceContinueToken(tc) + if err == nil { + t.Error("expected error for invalid token") + } + }) + } +} + +// --- Tests for parsePositiveIntQuery --- + +func TestParsePositiveIntQuery_ValidRange(t *testing.T) { + cases := []struct { + raw string + min, max int + want int + }{ + {"5", 1, 100, 5}, + {"1", 1, 100, 1}, // at min + {"100", 1, 100, 100}, // at max + } + for _, tc := range cases { + t.Run(fmt.Sprintf("%s_in_%d_%d", tc.raw, tc.min, tc.max), func(t *testing.T) { + got, err := parsePositiveIntQuery("test", tc.raw, tc.min, tc.max) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.want { + t.Errorf("got %d, want %d", got, tc.want) + } + }) + } +} + +func TestParsePositiveIntQuery_BelowMin(t *testing.T) { + _, err := parsePositiveIntQuery("test", "0", 1, 100) + if err == nil { + t.Error("expected error for value below min") + } +} + +func TestParsePositiveIntQuery_AboveMax(t *testing.T) { + _, err := parsePositiveIntQuery("test", "101", 1, 100) + if err == nil { + t.Error("expected error for value above max") + } +} + +func TestParsePositiveIntQuery_NonInteger(t *testing.T) { + _, err := parsePositiveIntQuery("test", "abc", 1, 100) + if err == nil { + t.Error("expected error for non-integer") + } +} + +// --- Tests for paginateCustomResourceItems --- + +func TestPaginateCustomResourceItems_BasicPage(t *testing.T) { + items := makeItems(10) + page, next := paginateCustomResourceItems(items, 0, 5) + if len(page) != 5 { + t.Errorf("got %d items, want 5", len(page)) + } + if next != "5" { + t.Errorf("got next=%q, want \"5\"", next) + } +} + +func TestPaginateCustomResourceItems_SecondPage(t *testing.T) { + items := makeItems(10) + page, next := paginateCustomResourceItems(items, 5, 5) + if len(page) != 5 { + t.Errorf("got %d items, want 5", len(page)) + } + if next != "" { + t.Errorf("got next=%q, want empty (last page)", next) + } +} + +func TestPaginateCustomResourceItems_OffsetPastEnd(t *testing.T) { + items := makeItems(5) + page, next := paginateCustomResourceItems(items, 10, 5) + if len(page) != 0 { + t.Errorf("got %d items, want 0", len(page)) + } + if next != "" { + t.Errorf("got next=%q, want empty", next) + } +} + +func TestPaginateCustomResourceItems_EmptyInput(t *testing.T) { + page, next := paginateCustomResourceItems(nil, 0, 10) + if len(page) != 0 { + t.Errorf("got %d items, want 0", len(page)) + } + if next != "" { + t.Errorf("got next=%q, want empty", next) + } +} + +func TestPaginateCustomResourceItems_ExactBoundary(t *testing.T) { + items := makeItems(10) + page, next := paginateCustomResourceItems(items, 0, 10) + if len(page) != 10 { + t.Errorf("got %d items, want 10", len(page)) + } + if next != "" { + t.Errorf("got next=%q, want empty (no more items)", next) + } +} + +func TestPaginateCustomResourceItems_PartialLastPage(t *testing.T) { + items := makeItems(7) + page, next := paginateCustomResourceItems(items, 5, 5) + if len(page) != 2 { + t.Errorf("got %d items, want 2", len(page)) + } + if next != "" { + t.Errorf("got next=%q, want empty", next) + } +} + +// --- Tests for parseCRItem --- + +func TestParseCRItem_CompleteObject(t *testing.T) { + obj := map[string]interface{}{ + "kind": "ScaledObject", + "metadata": map[string]interface{}{ + "name": "my-scaler", + "namespace": "default", + "labels": map[string]interface{}{ + "app": "web", + "env": "prod", + }, + }, + "spec": map[string]interface{}{ + "minReplicaCount": 1, + }, + "status": map[string]interface{}{ + "phase": "Active", + }, + } + item := parseCRItem(obj, "cluster-1") + if item.Name != "my-scaler" { + t.Errorf("Name=%q, want \"my-scaler\"", item.Name) + } + if item.Namespace != "default" { + t.Errorf("Namespace=%q, want \"default\"", item.Namespace) + } + if item.Kind != "ScaledObject" { + t.Errorf("Kind=%q, want \"ScaledObject\"", item.Kind) + } + if item.Cluster != "cluster-1" { + t.Errorf("Cluster=%q, want \"cluster-1\"", item.Cluster) + } + if item.Labels["app"] != "web" { + t.Errorf("Labels[app]=%q, want \"web\"", item.Labels["app"]) + } + if item.Spec == nil { + t.Error("Spec should not be nil") + } + if item.Status == nil { + t.Error("Status should not be nil") + } +} + +func TestParseCRItem_MinimalObject(t *testing.T) { + obj := map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "bare-resource", + }, + } + item := parseCRItem(obj, "cluster-2") + if item.Name != "bare-resource" { + t.Errorf("Name=%q, want \"bare-resource\"", item.Name) + } + if item.Namespace != "" { + t.Errorf("Namespace=%q, want empty", item.Namespace) + } + if item.Kind != "" { + t.Errorf("Kind=%q, want empty", item.Kind) + } + if item.Cluster != "cluster-2" { + t.Errorf("Cluster=%q, want \"cluster-2\"", item.Cluster) + } +} + +func TestParseCRItem_EmptyObject(t *testing.T) { + obj := map[string]interface{}{} + item := parseCRItem(obj, "cluster-3") + if item.Name != "" { + t.Errorf("Name=%q, want empty", item.Name) + } + if item.Cluster != "cluster-3" { + t.Errorf("Cluster=%q, want \"cluster-3\"", item.Cluster) + } +} + +func TestParseCRItem_NonStringLabels(t *testing.T) { + obj := map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "labeled", + "labels": map[string]interface{}{ + "valid": "yes", + "numeric": 42, // non-string label value should be skipped + }, + }, + } + item := parseCRItem(obj, "c1") + if item.Labels["valid"] != "yes" { + t.Errorf("Labels[valid]=%q, want \"yes\"", item.Labels["valid"]) + } + if _, ok := item.Labels["numeric"]; ok { + t.Error("non-string label should not appear in labels map") + } +} + +// --- Tests for minCustomResourceInt --- + +func TestMinCustomResourceInt(t *testing.T) { + cases := []struct { + a, b, want int + }{ + {1, 2, 1}, + {5, 3, 3}, + {7, 7, 7}, + {0, 1, 0}, + {-1, 0, -1}, + } + for _, tc := range cases { + t.Run(fmt.Sprintf("%d_%d", tc.a, tc.b), func(t *testing.T) { + if got := minCustomResourceInt(tc.a, tc.b); got != tc.want { + t.Errorf("got %d, want %d", got, tc.want) + } + }) + } +} + +// Helper to create N test items +func makeItems(n int) []CustomResourceItem { + items := make([]CustomResourceItem, n) + for i := 0; i < n; i++ { + items[i] = CustomResourceItem{ + Name: fmt.Sprintf("item-%d", i), + Cluster: "test-cluster", + } + } + return items +}