From e098e63b42b658b2d81eb9b7df64325806c5e3ff Mon Sep 17 00:00:00 2001 From: GitHub Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:21:54 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=B1=20[scanner]=20refactor:=20extract?= =?UTF-8?q?=20validation=20handlers=20to=20pkg/api/handlers/internal/httpu?= =?UTF-8?q?til=20(epic=20#23685=20phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the remaining cross-cutting input validators out of the flat pkg/api/handlers root package into the internal/httputil domain package, per the Phase-1 internal/httputil row of the epic plan. - validate_helpers.go + its test move via git mv; validators are exported (ValidateDNSLabel/ValidateDNSSubdomain/ValidateClusterName/ ValidateRoleName/ValidateEnum) so domain subpackages can reuse them. - Cron and Kubernetes API-version validators move to internal/httputil/validation.go (IsValidCronSchedule, IsValidK8sVersion, K8sVersionPattern), with their tests. - Root validation.go keeps the exported aliases MaxK8sNameLen, IsValidK8sName and IsValidK8sVersion used by pkg/api/handlers/mcp, and drops the now-dead unexported duplicates of httputil.ValidateK8sName / ValidateClusterAndNamespace. Behavior-preserving: no route or exported HTTP behavior changes. Coverage: pkg/api/handlers 93.8%, internal/httputil 100.0%. Refs #23685 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: GitHub Copilot <223556219+Copilot@users.noreply.github.com> --- .../httputil}/validate_helpers.go | 29 +- .../httputil}/validate_helpers_test.go | 20 +- .../handlers/internal/httputil/validation.go | 46 ++ .../internal/httputil/validation_test.go | 287 ++++++++++++ pkg/api/handlers/validation.go | 53 +-- pkg/api/handlers/validation_test.go | 411 +----------------- 6 files changed, 369 insertions(+), 477 deletions(-) rename pkg/api/handlers/{ => internal/httputil}/validate_helpers.go (83%) rename pkg/api/handlers/{ => internal/httputil}/validate_helpers_test.go (86%) create mode 100644 pkg/api/handlers/internal/httputil/validation.go create mode 100644 pkg/api/handlers/internal/httputil/validation_test.go diff --git a/pkg/api/handlers/validate_helpers.go b/pkg/api/handlers/internal/httputil/validate_helpers.go similarity index 83% rename from pkg/api/handlers/validate_helpers.go rename to pkg/api/handlers/internal/httputil/validate_helpers.go index 1643aac9f8..7473357abb 100644 --- a/pkg/api/handlers/validate_helpers.go +++ b/pkg/api/handlers/internal/httputil/validate_helpers.go @@ -1,9 +1,10 @@ -// Package handlers — input validation helpers shared by RBAC and namespace -// handlers. Added for #6627: the RBAC/namespace request structs previously had -// no field-level validation, so empty or malformed payloads silently relied on +// Field-level input validation helpers shared by RBAC and namespace handlers. +// Added for #6627: the RBAC/namespace request structs previously had no +// field-level validation, so empty or malformed payloads silently relied on // downstream Kubernetes API checks. These helpers centralise the rules so every // handler rejects bad input at the HTTP boundary with a specific 400 error. -package handlers +// Moved here from the root handlers package in epic #23685. +package httputil import ( "fmt" @@ -47,10 +48,10 @@ var dnsSubdomainRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a- // #6675 Copilot followup: allow any number of `:label` segments. var roleNameRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(:[a-z0-9]([-a-z0-9]*[a-z0-9])?)*(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) -// validateDNSLabel checks that s is a non-empty RFC 1123 DNS label suitable +// ValidateDNSLabel checks that s is a non-empty RFC 1123 DNS label suitable // for a Kubernetes object name (ServiceAccount, Namespace, RoleBinding, etc). // Returns a user-facing error that names the field. -func validateDNSLabel(field, s string) error { +func ValidateDNSLabel(field, s string) error { if s == "" { return fmt.Errorf("%s is required", field) } @@ -63,10 +64,10 @@ func validateDNSLabel(field, s string) error { return nil } -// validateDNSSubdomain checks that s is a non-empty RFC 1123 DNS subdomain. +// ValidateDNSSubdomain checks that s is a non-empty RFC 1123 DNS subdomain. // Used for fields that may legitimately contain dots (not currently used, // kept for future extensibility when we validate e.g. hostnames). -func validateDNSSubdomain(field, s string) error { +func ValidateDNSSubdomain(field, s string) error { if s == "" { return fmt.Errorf("%s is required", field) } @@ -79,10 +80,10 @@ func validateDNSSubdomain(field, s string) error { return nil } -// validateClusterName is a looser validator for cluster IDs. Cluster names +// ValidateClusterName is a looser validator for cluster IDs. Cluster names // in this codebase come from kubeconfig contexts and may contain dots, // dashes, slashes, and digits. We only enforce non-empty and length. -func validateClusterName(field, s string) error { +func ValidateClusterName(field, s string) error { if s == "" { return fmt.Errorf("%s is required", field) } @@ -95,9 +96,9 @@ func validateClusterName(field, s string) error { return nil } -// validateRoleName accepts a Kubernetes Role/ClusterRole name. These may +// ValidateRoleName accepts a Kubernetes Role/ClusterRole name. These may // contain a system: prefix and optional dots. -func validateRoleName(field, s string) error { +func ValidateRoleName(field, s string) error { if s == "" { return fmt.Errorf("%s is required", field) } @@ -110,9 +111,9 @@ func validateRoleName(field, s string) error { return nil } -// validateEnum checks that s is one of allowed (case-sensitive). Used for +// ValidateEnum checks that s is one of allowed (case-sensitive). Used for // fields like subjectKind, roleKind, and the namespace-access role shortcuts. -func validateEnum(field, s string, allowed []string) error { +func ValidateEnum(field, s string, allowed []string) error { if s == "" { return fmt.Errorf("%s is required", field) } diff --git a/pkg/api/handlers/validate_helpers_test.go b/pkg/api/handlers/internal/httputil/validate_helpers_test.go similarity index 86% rename from pkg/api/handlers/validate_helpers_test.go rename to pkg/api/handlers/internal/httputil/validate_helpers_test.go index 5b6e2a3ecd..3c2f5915fd 100644 --- a/pkg/api/handlers/validate_helpers_test.go +++ b/pkg/api/handlers/internal/httputil/validate_helpers_test.go @@ -1,4 +1,4 @@ -package handlers +package httputil import ( "strings" @@ -28,7 +28,7 @@ func TestValidateDNSLabel(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := validateDNSLabel("field", tc.value) + err := ValidateDNSLabel("field", tc.value) if tc.wantErr && err == nil { t.Fatalf("expected error, got nil") } @@ -56,10 +56,11 @@ func TestValidateClusterName(t *testing.T) { {"empty", "", true}, {"newline", "prod\nhacked", true}, {"tab", "prod\thacked", true}, + {"too long", strings.Repeat("a", maxK8sDNSSubdomainLen+1), true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := validateClusterName("cluster", tc.value) + err := ValidateClusterName("cluster", tc.value) if tc.wantErr && err == nil { t.Fatalf("expected error") } @@ -84,10 +85,11 @@ func TestValidateRoleName(t *testing.T) { {"dotted", "rbac.example.com", false}, {"empty", "", true}, {"uppercase", "Admin", true}, + {"too long", strings.Repeat("a", maxRoleNameLen+1), true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := validateRoleName("role", tc.value) + err := ValidateRoleName("role", tc.value) if tc.wantErr && err == nil { t.Fatalf("expected error") } @@ -101,18 +103,18 @@ func TestValidateRoleName(t *testing.T) { // TestValidateEnum covers accept/reject and the empty-string required case. func TestValidateEnum(t *testing.T) { allowed := []string{"User", "Group", "ServiceAccount"} - if err := validateEnum("subjectKind", "User", allowed); err != nil { + if err := ValidateEnum("subjectKind", "User", allowed); err != nil { t.Fatalf("unexpected: %v", err) } - if err := validateEnum("subjectKind", "pod", allowed); err == nil { + if err := ValidateEnum("subjectKind", "pod", allowed); err == nil { t.Fatalf("expected error for disallowed value") } - if err := validateEnum("subjectKind", "", allowed); err == nil { + if err := ValidateEnum("subjectKind", "", allowed); err == nil { t.Fatalf("expected error for empty value") } } -// TestValidateDNSSubdomain exercises validateDNSSubdomain's empty, too-long, +// TestValidateDNSSubdomain exercises ValidateDNSSubdomain's empty, too-long, // invalid-char, and valid (including multi-label) cases. func TestValidateDNSSubdomain(t *testing.T) { cases := []struct { @@ -131,7 +133,7 @@ func TestValidateDNSSubdomain(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := validateDNSSubdomain("field", tc.value) + err := ValidateDNSSubdomain("field", tc.value) if tc.wantErr && err == nil { t.Fatalf("expected error, got nil") } diff --git a/pkg/api/handlers/internal/httputil/validation.go b/pkg/api/handlers/internal/httputil/validation.go new file mode 100644 index 0000000000..51e13943f7 --- /dev/null +++ b/pkg/api/handlers/internal/httputil/validation.go @@ -0,0 +1,46 @@ +// Cron-expression and Kubernetes API-version validators. Moved here from the +// root handlers package in epic #23685 so domain subpackages can share them. +package httputil + +import ( + "regexp" + "strings" +) + +// cronFieldCount is the number of fields in a standard cron expression. +const cronFieldCount = 5 + +// cronFieldPattern matches a single cron field (digits, *, /, -, comma). +var cronFieldPattern = regexp.MustCompile(`^[\d\*,/\-]+$`) + +// K8sVersionPattern matches Kubernetes API versions (e.g. "v1", "v1beta1", "v2alpha1"). +var K8sVersionPattern = regexp.MustCompile(`^v[0-9]+([a-z]+[0-9]+)?$`) + +// maxCronFieldLen is the maximum length of a single cron field to prevent abuse. +const maxCronFieldLen = 64 + +// IsValidCronSchedule validates a 5-field cron expression. +// It does not validate semantic correctness (e.g. day 32), only structural format. +func IsValidCronSchedule(schedule string) bool { + fields := strings.Fields(schedule) + if len(fields) != cronFieldCount { + return false + } + for _, f := range fields { + if len(f) > maxCronFieldLen { + return false + } + if !cronFieldPattern.MatchString(f) { + return false + } + } + return true +} + +// IsValidK8sVersion validates a Kubernetes API version string. +func IsValidK8sVersion(version string) bool { + if len(version) > MaxK8sNameLen { + return false + } + return K8sVersionPattern.MatchString(version) +} diff --git a/pkg/api/handlers/internal/httputil/validation_test.go b/pkg/api/handlers/internal/httputil/validation_test.go new file mode 100644 index 0000000000..a6e0670845 --- /dev/null +++ b/pkg/api/handlers/internal/httputil/validation_test.go @@ -0,0 +1,287 @@ +package httputil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidCronSchedule(t *testing.T) { + tests := []struct { + name string + schedule string + wantOK bool + }{ + { + name: "ValidEveryMinute", + schedule: "* * * * *", + wantOK: true, + }, + { + name: "ValidSpecificTime", + schedule: "0 12 * * *", + wantOK: true, + }, + { + name: "ValidWithRanges", + schedule: "0-30 9-17 * * 1-5", + wantOK: true, + }, + { + name: "ValidWithStep", + schedule: "*/15 * * * *", + wantOK: true, + }, + { + name: "ValidWithComma", + schedule: "0,30 * * * *", + wantOK: true, + }, + { + name: "TooFewFields", + schedule: "* * *", + wantOK: false, + }, + { + name: "TooManyFields", + schedule: "* * * * * *", + wantOK: false, + }, + { + name: "InvalidCharacter", + schedule: "* * @ * *", + wantOK: false, + }, + { + name: "FieldTooLong", + schedule: "* * * * " + string(make([]byte, maxCronFieldLen+1)), + wantOK: false, + }, + { + name: "EmptyString", + schedule: "", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidCronSchedule(tt.schedule) + assert.Equal(t, tt.wantOK, result) + }) + } +} + +func TestIsValidK8sVersion(t *testing.T) { + tests := []struct { + name string + version string + wantOK bool + }{ + { + name: "ValidV1", + version: "v1", + wantOK: true, + }, + { + name: "ValidV2", + version: "v2", + wantOK: true, + }, + { + name: "ValidBeta", + version: "v1beta1", + wantOK: true, + }, + { + name: "ValidAlpha", + version: "v1alpha1", + wantOK: true, + }, + { + name: "ValidBeta2", + version: "v2beta2", + wantOK: true, + }, + { + name: "NoV", + version: "1", + wantOK: false, + }, + { + name: "Uppercase", + version: "V1", + wantOK: false, + }, + { + name: "NoNumber", + version: "v", + wantOK: false, + }, + { + name: "InvalidSuffix", + version: "v1-beta", + wantOK: false, + }, + { + name: "TooLong", + version: string(make([]byte, MaxK8sNameLen+1)), + wantOK: false, + }, + { + name: "EmptyString", + version: "", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidK8sVersion(tt.version) + assert.Equal(t, tt.wantOK, result) + }) + } +} + +func TestCronFieldPatternRegex(t *testing.T) { + tests := []struct { + name string + field string + wantOK bool + }{ + { + name: "Asterisk", + field: "*", + wantOK: true, + }, + { + name: "Number", + field: "5", + wantOK: true, + }, + { + name: "Range", + field: "1-5", + wantOK: true, + }, + { + name: "Step", + field: "*/5", + wantOK: true, + }, + { + name: "Comma", + field: "1,3,5", + wantOK: true, + }, + { + name: "InvalidChar", + field: "@", + wantOK: false, + }, + { + name: "Letter", + field: "a", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := cronFieldPattern.MatchString(tt.field) + assert.Equal(t, tt.wantOK, result) + }) + } +} + +func TestK8sNamePatternRegex(t *testing.T) { + tests := []struct { + name string + input string + wantOK bool + }{ + { + name: "SimpleName", + input: "apps", + wantOK: true, + }, + { + name: "WithDashes", + input: "kube-system", + wantOK: true, + }, + { + name: "WithDots", + input: "v1.beta1", + wantOK: true, + }, + { + name: "SingleChar", + input: "a", + wantOK: true, + }, + { + name: "Uppercase", + input: "Apps", + wantOK: false, + }, + { + name: "StartsWithDash", + input: "-apps", + wantOK: false, + }, + { + name: "EndsWithDash", + input: "apps-", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := K8sNamePattern.MatchString(tt.input) + assert.Equal(t, tt.wantOK, result) + }) + } +} + +func TestK8sVersionPatternRegex(t *testing.T) { + tests := []struct { + name string + version string + wantOK bool + }{ + { + name: "V1", + version: "v1", + wantOK: true, + }, + { + name: "V1Beta1", + version: "v1beta1", + wantOK: true, + }, + { + name: "V1Alpha1", + version: "v1alpha1", + wantOK: true, + }, + { + name: "NoV", + version: "1", + wantOK: false, + }, + { + name: "Uppercase", + version: "V1", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := K8sVersionPattern.MatchString(tt.version) + assert.Equal(t, tt.wantOK, result) + }) + } +} diff --git a/pkg/api/handlers/validation.go b/pkg/api/handlers/validation.go index 28440dd335..14eb6661a2 100644 --- a/pkg/api/handlers/validation.go +++ b/pkg/api/handlers/validation.go @@ -1,45 +1,9 @@ package handlers import ( - "regexp" - "strings" - "github.com/kubestellar/console/pkg/api/handlers/internal/httputil" ) -// cronFieldCount is the number of fields in a standard cron expression. -const cronFieldCount = 5 - -// cronFieldPattern matches a single cron field (digits, *, /, -, comma). -var cronFieldPattern = regexp.MustCompile(`^[\d\*,/\-]+$`) - -// k8sNamePattern aliases httputil.K8sNamePattern (moved in epic #23685). -var k8sNamePattern = httputil.K8sNamePattern - -// k8sVersionPattern matches Kubernetes API versions (e.g. "v1", "v1beta1", "v2alpha1"). -var k8sVersionPattern = regexp.MustCompile(`^v[0-9]+([a-z]+[0-9]+)?$`) - -// maxCronFieldLen is the maximum length of a single cron field to prevent abuse. -const maxCronFieldLen = 64 - -// isValidCronSchedule validates a 5-field cron expression. -// It does not validate semantic correctness (e.g. day 32), only structural format. -func isValidCronSchedule(schedule string) bool { - fields := strings.Fields(schedule) - if len(fields) != cronFieldCount { - return false - } - for _, f := range fields { - if len(f) > maxCronFieldLen { - return false - } - if !cronFieldPattern.MatchString(f) { - return false - } - } - return true -} - // MaxK8sNameLen is the maximum length for a Kubernetes resource name (DNS-1123 subdomain). const MaxK8sNameLen = httputil.MaxK8sNameLen @@ -48,20 +12,7 @@ func IsValidK8sName(name string) bool { return httputil.IsValidK8sName(name) } -// isValidK8sVersion validates a Kubernetes API version string. +// IsValidK8sVersion delegates to httputil.IsValidK8sVersion (moved in epic #23685). func IsValidK8sVersion(version string) bool { - if len(version) > MaxK8sNameLen { - return false - } - return k8sVersionPattern.MatchString(version) -} - -// validateK8sName delegates to httputil.ValidateK8sName (moved in epic #23685). -func validateK8sName(param, value string) error { - return httputil.ValidateK8sName(param, value) -} - -// validateClusterAndNamespace delegates to httputil.ValidateClusterAndNamespace. -func validateClusterAndNamespace(cluster, namespace string) error { - return httputil.ValidateClusterAndNamespace(cluster, namespace) + return httputil.IsValidK8sVersion(version) } diff --git a/pkg/api/handlers/validation_test.go b/pkg/api/handlers/validation_test.go index f4d08a61f0..dfa1c8bd0a 100644 --- a/pkg/api/handlers/validation_test.go +++ b/pkg/api/handlers/validation_test.go @@ -4,75 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestIsValidCronSchedule(t *testing.T) { - tests := []struct { - name string - schedule string - wantOK bool - }{ - { - name: "ValidEveryMinute", - schedule: "* * * * *", - wantOK: true, - }, - { - name: "ValidSpecificTime", - schedule: "0 12 * * *", - wantOK: true, - }, - { - name: "ValidWithRanges", - schedule: "0-30 9-17 * * 1-5", - wantOK: true, - }, - { - name: "ValidWithStep", - schedule: "*/15 * * * *", - wantOK: true, - }, - { - name: "ValidWithComma", - schedule: "0,30 * * * *", - wantOK: true, - }, - { - name: "TooFewFields", - schedule: "* * *", - wantOK: false, - }, - { - name: "TooManyFields", - schedule: "* * * * * *", - wantOK: false, - }, - { - name: "InvalidCharacter", - schedule: "* * @ * *", - wantOK: false, - }, - { - name: "FieldTooLong", - schedule: "* * * * " + string(make([]byte, maxCronFieldLen+1)), - wantOK: false, - }, - { - name: "EmptyString", - schedule: "", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := isValidCronSchedule(tt.schedule) - assert.Equal(t, tt.wantOK, result) - }) - } -} - func TestIsValidK8sName(t *testing.T) { tests := []struct { name string @@ -159,345 +92,17 @@ func TestIsValidK8sName(t *testing.T) { } } -func TestIsValidK8sVersion(t *testing.T) { - tests := []struct { - name string - version string - wantOK bool - }{ - { - name: "ValidV1", - version: "v1", - wantOK: true, - }, - { - name: "ValidV2", - version: "v2", - wantOK: true, - }, - { - name: "ValidBeta", - version: "v1beta1", - wantOK: true, - }, - { - name: "ValidAlpha", - version: "v1alpha1", - wantOK: true, - }, - { - name: "ValidBeta2", - version: "v2beta2", - wantOK: true, - }, - { - name: "NoV", - version: "1", - wantOK: false, - }, - { - name: "Uppercase", - version: "V1", - wantOK: false, - }, - { - name: "NoNumber", - version: "v", - wantOK: false, - }, - { - name: "InvalidSuffix", - version: "v1-beta", - wantOK: false, - }, - { - name: "TooLong", - version: string(make([]byte, MaxK8sNameLen+1)), - wantOK: false, - }, - { - name: "EmptyString", - version: "", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := IsValidK8sVersion(tt.version) - assert.Equal(t, tt.wantOK, result) - }) - } -} - func TestMaxK8sNameLenConstant(t *testing.T) { // Verify the constant is defined and has the expected value assert.Equal(t, 253, MaxK8sNameLen) } -func TestCronFieldPatternRegex(t *testing.T) { - tests := []struct { - name string - field string - wantOK bool - }{ - { - name: "Asterisk", - field: "*", - wantOK: true, - }, - { - name: "Number", - field: "5", - wantOK: true, - }, - { - name: "Range", - field: "1-5", - wantOK: true, - }, - { - name: "Step", - field: "*/5", - wantOK: true, - }, - { - name: "Comma", - field: "1,3,5", - wantOK: true, - }, - { - name: "InvalidChar", - field: "@", - wantOK: false, - }, - { - name: "Letter", - field: "a", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := cronFieldPattern.MatchString(tt.field) - assert.Equal(t, tt.wantOK, result) - }) - } -} - -func TestK8sNamePatternRegex(t *testing.T) { - tests := []struct { - name string - input string - wantOK bool - }{ - { - name: "SimpleName", - input: "apps", - wantOK: true, - }, - { - name: "WithDashes", - input: "kube-system", - wantOK: true, - }, - { - name: "WithDots", - input: "v1.beta1", - wantOK: true, - }, - { - name: "SingleChar", - input: "a", - wantOK: true, - }, - { - name: "Uppercase", - input: "Apps", - wantOK: false, - }, - { - name: "StartsWithDash", - input: "-apps", - wantOK: false, - }, - { - name: "EndsWithDash", - input: "apps-", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := k8sNamePattern.MatchString(tt.input) - assert.Equal(t, tt.wantOK, result) - }) - } -} - -func TestK8sVersionPatternRegex(t *testing.T) { - tests := []struct { - name string - version string - wantOK bool - }{ - { - name: "V1", - version: "v1", - wantOK: true, - }, - { - name: "V1Beta1", - version: "v1beta1", - wantOK: true, - }, - { - name: "V1Alpha1", - version: "v1alpha1", - wantOK: true, - }, - { - name: "NoV", - version: "1", - wantOK: false, - }, - { - name: "Uppercase", - version: "V1", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := k8sVersionPattern.MatchString(tt.version) - assert.Equal(t, tt.wantOK, result) - }) - } -} - -func TestValidateK8sName(t *testing.T) { - tests := []struct { - name string - param string - value string - wantErr bool - errContains string - }{ - { - name: "EmptyValueAllowed", - param: "namespace", - value: "", - wantErr: false, - }, - { - name: "ValidLowercase", - param: "namespace", - value: "kube-system", - wantErr: false, - }, - { - name: "ValidWithDots", - param: "group", - value: "apps.v1", - wantErr: false, - }, - { - name: "ValidSingleChar", - param: "namespace", - value: "a", - wantErr: false, - }, - { - name: "TooLong", - param: "namespace", - value: string(make([]byte, MaxK8sNameLen+1)), - wantErr: true, - errContains: "namespace", - }, - { - name: "InvalidUppercase", - param: "cluster", - value: "MyCluster", - wantErr: true, - errContains: "cluster", - }, - { - name: "InvalidUnderscore", - param: "namespace", - value: "my_namespace", - wantErr: true, - errContains: "must be a valid Kubernetes resource name", - }, - { - name: "InvalidStartsWithDash", - param: "namespace", - value: "-namespace", - wantErr: true, - errContains: "must be a valid Kubernetes resource name", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateK8sName(tt.param, tt.value) - if tt.wantErr { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errContains) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestValidateClusterAndNamespace(t *testing.T) { - tests := []struct { - name string - cluster string - namespace string - wantErr bool - errContains string - }{ - { - name: "BothEmpty", - cluster: "", - namespace: "", - wantErr: false, - }, - { - name: "BothValid", - cluster: "my-cluster", - namespace: "kube-system", - wantErr: false, - }, - { - name: "InvalidCluster", - cluster: "Invalid_Cluster", - namespace: "kube-system", - wantErr: true, - errContains: "cluster", - }, - { - name: "ValidClusterInvalidNamespace", - cluster: "my-cluster", - namespace: "Invalid_Namespace", - wantErr: true, - errContains: "namespace", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateClusterAndNamespace(tt.cluster, tt.namespace) - if tt.wantErr { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errContains) - } else { - assert.NoError(t, err) - } - }) - } +// TestIsValidK8sVersionAlias checks that the root alias still delegates to +// httputil for both accepted and rejected version strings. The full table +// lives in internal/httputil/validation_test.go. +func TestIsValidK8sVersionAlias(t *testing.T) { + assert.True(t, IsValidK8sVersion("v1")) + assert.True(t, IsValidK8sVersion("v1beta1")) + assert.False(t, IsValidK8sVersion("V1")) + assert.False(t, IsValidK8sVersion("")) }