Files
kportal/internal/config/validator_test.go
T
lukaszraczylo c413b808f1 fix(config): allow @ . : / in context names
Real kubeconfig context names commonly contain characters the
validator rejected:
  - 'admin@home', 'user@cluster.example.com' (kubectl rename, EKS
    aws-iam-authenticator)
  - 'cluster.example.com', 'gke_proj_zone_cluster.prod' (FQDN, GKE)
  - 'arn:aws:eks:us-east-1:123:cluster/foo' (EKS ARN)

kubeconfig itself imposes no character restrictions, so requiring
[a-zA-Z0-9_-] only was kportal-specific over-validation that blocked
legitimate users. Widen the allowed set to add '@', '.', ':', '/'.
Names must still start and end with a letter or digit so YAML
specials and leading whitespace remain rejected.

Tests cover the new positive cases and tighten negative coverage
(starts-with-@, ends-with-/, ends-with-dot).
2026-05-06 12:11:37 +01:00

2053 lines
55 KiB
Go

package config
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidator_ValidateConfig(t *testing.T) {
validator := NewValidator()
tests := []struct {
config *Config
name string
errorContains []string
expectErrors bool
}{
{
name: "valid config",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/my-app",
Protocol: "tcp",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
expectErrors: false,
},
{
name: "nil config",
config: nil,
expectErrors: true,
errorContains: []string{"Configuration is nil"},
},
{
name: "empty contexts",
config: &Config{
Contexts: []Context{},
},
expectErrors: true,
errorContains: []string{"must have at least one context"},
},
{
name: "empty namespaces",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{},
},
},
},
expectErrors: true,
errorContains: []string{"must have at least one namespace"},
},
{
name: "empty forwards",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"must have at least one forward"},
},
{
name: "invalid port - zero",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/my-app",
Protocol: "tcp",
Port: 0,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Invalid port 0"},
},
{
name: "invalid port - above max",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/my-app",
Protocol: "tcp",
Port: 8080,
LocalPort: 65536,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Invalid localPort 65536"},
},
{
name: "invalid protocol",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/my-app",
Protocol: "http",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Invalid protocol 'http'", "only 'tcp' is supported"},
},
{
name: "empty resource",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "",
Protocol: "tcp",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Resource cannot be empty"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.ValidateConfig(tt.config)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
// Check that expected error messages are present
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found", expectedMsg)
}
} else {
assert.Empty(t, errs, "expected no validation errors")
}
})
}
}
func TestValidator_ValidateResourceFormat(t *testing.T) {
validator := NewValidator()
tests := []struct {
name string
errorContains []string
forward Forward
expectErrors bool
}{
{
name: "valid pod with name",
forward: Forward{
Resource: "pod/my-app",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: false,
},
{
name: "valid service with name",
forward: Forward{
Resource: "service/postgres",
Port: 5432,
LocalPort: 5432,
contextName: "dev",
namespaceName: "default",
},
expectErrors: false,
},
{
name: "valid pod with selector (no name)",
forward: Forward{
Resource: "pod",
Selector: "app=nginx",
Port: 80,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: false,
},
{
name: "invalid resource type",
forward: Forward{
Resource: "deployment/my-app",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"Invalid resource type 'deployment'"},
},
{
name: "pod with name and selector (invalid)",
forward: Forward{
Resource: "pod/my-app",
Selector: "app=nginx",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"should not have a selector"},
},
{
name: "pod without name and without selector (invalid)",
forward: Forward{
Resource: "pod",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"must have a selector"},
},
{
name: "service without name (invalid)",
forward: Forward{
Resource: "service",
Port: 5432,
LocalPort: 5432,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"Service name cannot be empty"},
},
{
name: "service with selector (invalid)",
forward: Forward{
Resource: "service/postgres",
Selector: "app=db",
Port: 5432,
LocalPort: 5432,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"should not have a selector"},
},
{
name: "pod with empty name after slash",
forward: Forward{
Resource: "pod/",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"Pod name cannot be empty"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.validateForward(&tt.forward)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
// Check that expected error messages are present
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found", expectedMsg)
}
} else {
assert.Empty(t, errs, "expected no validation errors")
}
})
}
}
func TestValidator_CheckDuplicatePorts(t *testing.T) {
validator := NewValidator()
tests := []struct {
config *Config
name string
errorContains []string
expectErrors bool
}{
{
name: "no duplicate ports",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/app1",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
{
Resource: "pod/app2",
Port: 8081,
LocalPort: 8081,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
expectErrors: false,
},
{
name: "duplicate ports in same namespace",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/app1",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
{
Resource: "pod/app2",
Port: 8081,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Duplicate local port 8080"},
},
{
name: "duplicate ports across namespaces",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "ns1",
Forwards: []Forward{
{
Resource: "pod/app1",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "ns1",
},
},
},
{
Name: "ns2",
Forwards: []Forward{
{
Resource: "pod/app2",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "ns2",
},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Duplicate local port 8080"},
},
{
name: "duplicate ports across contexts",
config: &Config{
Contexts: []Context{
{
Name: "cluster1",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/app1",
Port: 8080,
LocalPort: 8080,
contextName: "cluster1",
namespaceName: "default",
},
},
},
},
},
{
Name: "cluster2",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/app2",
Port: 8080,
LocalPort: 8080,
contextName: "cluster2",
namespaceName: "default",
},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Duplicate local port 8080"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.validateDuplicatePorts(tt.config)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
// Check that expected error messages are present
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found", expectedMsg)
}
} else {
assert.Empty(t, errs, "expected no validation errors")
}
})
}
}
func TestFormatValidationErrors(t *testing.T) {
tests := []struct {
name string
errors []ValidationError
expectContains []string
expectEmpty bool
}{
{
name: "no errors",
errors: []ValidationError{},
expectEmpty: true,
},
{
name: "single error",
errors: []ValidationError{
{
Field: "port",
Message: "Invalid port 0",
},
},
expectEmpty: false,
expectContains: []string{"Configuration Validation Errors", "1. Invalid port 0"},
},
{
name: "multiple errors",
errors: []ValidationError{
{
Field: "port",
Message: "Invalid port 0",
},
{
Field: "resource",
Message: "Resource cannot be empty",
},
},
expectEmpty: false,
expectContains: []string{"Configuration Validation Errors", "1. Invalid port 0", "2. Resource cannot be empty"},
},
{
name: "error with context",
errors: []ValidationError{
{
Field: "localPort",
Message: "Duplicate local port 8080",
Context: map[string]string{
"port": "8080",
"forwards": "dev/default/pod/app1:8080, dev/default/pod/app2:8080",
},
},
},
expectEmpty: false,
expectContains: []string{"Configuration Validation Errors", "Duplicate local port 8080", "port:", "8080", "forwards:"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := FormatValidationErrors(tt.errors)
if tt.expectEmpty {
assert.Empty(t, output, "expected empty output")
} else {
assert.NotEmpty(t, output, "expected non-empty output")
// Check that expected strings are present
for _, expected := range tt.expectContains {
assert.Contains(t, output, expected, "output should contain '%s'", expected)
}
}
})
}
}
func TestValidator_ValidateStructure(t *testing.T) {
validator := NewValidator()
tests := []struct {
config *Config
name string
errorContains []string
expectErrors bool
}{
{
name: "empty context name",
config: &Config{
Contexts: []Context{
{
Name: "",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Context name cannot be empty"},
},
{
name: "empty namespace name",
config: &Config{
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Namespace name cannot be empty"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.validateStructure(tt.config)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
// Check that expected error messages are present
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found", expectedMsg)
}
} else {
assert.Empty(t, errs, "expected no validation errors")
}
})
}
}
func TestValidator_ValidateMDNS(t *testing.T) {
validator := NewValidator()
tests := []struct {
config *Config
name string
errorContains []string
expectErrors bool
}{
{
name: "mDNS disabled - no validation",
config: &Config{
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app", Port: 8080, LocalPort: 8080, Alias: "invalid_alias", contextName: "dev", namespaceName: "default"},
},
},
},
},
},
},
expectErrors: false,
},
{
name: "mDNS enabled - valid aliases",
config: &Config{
MDNS: &MDNSSpec{Enabled: true},
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app1", Port: 8080, LocalPort: 8080, Alias: "my-app", contextName: "dev", namespaceName: "default"},
{Resource: "pod/app2", Port: 8081, LocalPort: 8081, Alias: "my-service", contextName: "dev", namespaceName: "default"},
},
},
},
},
},
},
expectErrors: false,
},
{
name: "mDNS enabled - no alias (allowed)",
config: &Config{
MDNS: &MDNSSpec{Enabled: true},
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app", Port: 8080, LocalPort: 8080, contextName: "dev", namespaceName: "default"},
},
},
},
},
},
},
expectErrors: false,
},
{
name: "mDNS enabled - invalid alias with underscore",
config: &Config{
MDNS: &MDNSSpec{Enabled: true},
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app", Port: 8080, LocalPort: 8080, Alias: "my_app", contextName: "dev", namespaceName: "default"},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"invalid mDNS hostname", "RFC 1123"},
},
{
name: "mDNS enabled - alias starts with hyphen",
config: &Config{
MDNS: &MDNSSpec{Enabled: true},
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app", Port: 8080, LocalPort: 8080, Alias: "-myapp", contextName: "dev", namespaceName: "default"},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"invalid mDNS hostname"},
},
{
name: "mDNS enabled - alias ends with hyphen",
config: &Config{
MDNS: &MDNSSpec{Enabled: true},
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app", Port: 8080, LocalPort: 8080, Alias: "myapp-", contextName: "dev", namespaceName: "default"},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"invalid mDNS hostname"},
},
{
name: "mDNS enabled - duplicate aliases",
config: &Config{
MDNS: &MDNSSpec{Enabled: true},
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app1", Port: 8080, LocalPort: 8080, Alias: "myapp", contextName: "dev", namespaceName: "default"},
{Resource: "pod/app2", Port: 8081, LocalPort: 8081, Alias: "myapp", contextName: "dev", namespaceName: "default"},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Duplicate mDNS hostname", "conflict"},
},
{
name: "mDNS enabled - duplicate aliases across contexts",
config: &Config{
MDNS: &MDNSSpec{Enabled: true},
Contexts: []Context{
{
Name: "cluster1",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app1", Port: 8080, LocalPort: 8080, Alias: "shared-name", contextName: "cluster1", namespaceName: "default"},
},
},
},
},
{
Name: "cluster2",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app2", Port: 8081, LocalPort: 8081, Alias: "shared-name", contextName: "cluster2", namespaceName: "default"},
},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"Duplicate mDNS hostname", "shared-name"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.ValidateConfig(tt.config)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
// Check that expected error messages are present
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found in errors: %v", expectedMsg, errs)
}
} else {
assert.Empty(t, errs, "expected no validation errors, got: %v", errs)
}
})
}
}
func TestIsValidHostname(t *testing.T) {
tests := []struct {
name string
hostname string
valid bool
}{
{name: "valid simple", hostname: "myservice", valid: true},
{name: "valid with hyphen", hostname: "my-service", valid: true},
{name: "valid with numbers", hostname: "service123", valid: true},
{name: "valid mixed", hostname: "my-service-123", valid: true},
{name: "valid uppercase", hostname: "MyService", valid: true},
{name: "valid single char", hostname: "a", valid: true},
{name: "valid single digit", hostname: "1", valid: true},
{name: "valid max length (63)", hostname: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", valid: true},
{name: "invalid empty", hostname: "", valid: false},
{name: "invalid too long (64)", hostname: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", valid: false},
{name: "invalid starts with hyphen", hostname: "-myservice", valid: false},
{name: "invalid ends with hyphen", hostname: "myservice-", valid: false},
{name: "invalid underscore", hostname: "my_service", valid: false},
{name: "invalid dot", hostname: "my.service", valid: false},
{name: "invalid space", hostname: "my service", valid: false},
{name: "invalid special char", hostname: "my@service", valid: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isValidHostname(tt.hostname)
assert.Equal(t, tt.valid, result, "isValidHostname(%q) = %v, want %v", tt.hostname, result, tt.valid)
})
}
}
func TestIsAlphanumeric(t *testing.T) {
tests := []struct {
char byte
valid bool
}{
{char: 'a', valid: true},
{char: 'z', valid: true},
{char: 'A', valid: true},
{char: 'Z', valid: true},
{char: '0', valid: true},
{char: '9', valid: true},
{char: '-', valid: false},
{char: '_', valid: false},
{char: '.', valid: false},
{char: ' ', valid: false},
{char: '@', valid: false},
}
for _, tt := range tests {
t.Run(string(tt.char), func(t *testing.T) {
result := isAlphanumeric(tt.char)
assert.Equal(t, tt.valid, result, "isAlphanumeric(%q) = %v, want %v", tt.char, result, tt.valid)
})
}
}
func TestValidator_ValidateConfigWithOptions(t *testing.T) {
validator := NewValidator()
tests := []struct {
config *Config
name string
allowEmpty bool
expectErrors bool
}{
{
name: "empty config - strict mode",
config: &Config{Contexts: []Context{}},
allowEmpty: false,
expectErrors: true,
},
{
name: "empty config - allow empty",
config: &Config{Contexts: []Context{}},
allowEmpty: true,
expectErrors: false,
},
{
name: "nil contexts - allow empty",
config: &Config{},
allowEmpty: true,
expectErrors: false,
},
{
name: "context with no forwards - allow empty",
config: &Config{
Contexts: []Context{
{
Name: "dev",
Namespaces: []Namespace{
{Name: "default", Forwards: []Forward{}},
},
},
},
},
allowEmpty: true,
expectErrors: false,
},
{
name: "valid config - strict mode",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/my-app",
Protocol: "tcp",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
allowEmpty: false,
expectErrors: false,
},
{
name: "valid config - allow empty (should still validate)",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/my-app",
Protocol: "tcp",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
allowEmpty: true,
expectErrors: false,
},
{
name: "invalid forward in non-empty config - allow empty still validates",
config: &Config{
Contexts: []Context{
{
Name: "dev-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{
Resource: "pod/my-app",
Protocol: "tcp",
Port: 0, // Invalid port
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
},
},
},
},
},
},
allowEmpty: true,
expectErrors: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.ValidateConfigWithOptions(tt.config, tt.allowEmpty)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
} else {
assert.Empty(t, errs, "expected no validation errors, got: %v", errs)
}
})
}
}
func TestValidatePort(t *testing.T) {
tests := []struct {
name string
portName string
port int
expectError bool
}{
{name: "valid port - minimum", portName: "port", port: 1, expectError: false},
{name: "valid port - maximum", portName: "port", port: 65535, expectError: false},
{name: "valid port - middle", portName: "port", port: 8080, expectError: false},
{name: "invalid port - zero", portName: "port", port: 0, expectError: true},
{name: "invalid port - negative", portName: "port", port: -1, expectError: true},
{name: "invalid port - too high", portName: "port", port: 65536, expectError: true},
{name: "invalid port - very high", portName: "localPort", port: 100000, expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidatePort(tt.port, tt.portName)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.portName)
assert.Contains(t, err.Error(), fmt.Sprintf("%d", tt.port))
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateResourceFormat(t *testing.T) {
tests := []struct {
name string
resource string
errorMsg string
expectError bool
}{
{name: "valid pod", resource: "pod/my-app", errorMsg: "", expectError: false},
{name: "valid service", resource: "service/my-service", errorMsg: "", expectError: false},
{name: "valid pod with subdomain", resource: "pod/my-app.example.com", errorMsg: "", expectError: false},
{name: "missing slash", resource: "pod", errorMsg: "must be in format 'type/name'", expectError: true},
{name: "empty string", resource: "", errorMsg: "must be in format 'type/name'", expectError: true},
{name: "invalid type", resource: "deployment/my-app", errorMsg: "invalid resource type", expectError: true},
{name: "empty name", resource: "pod/", errorMsg: "resource name cannot be empty", expectError: true},
{name: "multiple slashes", resource: "pod/name/extra", errorMsg: "", expectError: false}, // First slash separates type/name, rest is part of name
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateResourceFormat(tt.resource)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateDuration(t *testing.T) {
tests := []struct {
name string
duration string
durationName string
expectError bool
}{
{name: "valid seconds", duration: "10s", durationName: "interval", expectError: false},
{name: "valid minutes", duration: "5m", durationName: "timeout", expectError: false},
{name: "valid hours", duration: "1h", durationName: "maxAge", expectError: false},
{name: "valid milliseconds", duration: "500ms", durationName: "timeout", expectError: false},
{name: "valid complex", duration: "1h30m", durationName: "duration", expectError: false},
{name: "empty string", duration: "", durationName: "interval", expectError: false}, // Empty is allowed (uses default)
{name: "invalid - no unit", duration: "10", durationName: "interval", expectError: true},
{name: "invalid - bad format", duration: "abc", durationName: "timeout", expectError: true},
{name: "invalid - unknown unit", duration: "10x", durationName: "interval", expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateDuration(tt.duration, tt.durationName)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.durationName)
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateDNS1123Label(t *testing.T) {
tests := []struct {
name string
label string
errorMsg string
expectError bool
}{
{name: "valid simple", label: "myname", errorMsg: "", expectError: false},
{name: "valid with hyphen", label: "my-name", errorMsg: "", expectError: false},
{name: "valid with numbers", label: "name123", errorMsg: "", expectError: false},
{name: "valid single char", label: "a", errorMsg: "", expectError: false},
{name: "valid max length", label: strings.Repeat("a", 63), errorMsg: "", expectError: false},
{name: "invalid empty", label: "", errorMsg: "not a valid DNS label", expectError: true},
{name: "invalid uppercase", label: "MyName", errorMsg: "not a valid DNS label", expectError: true},
{name: "invalid underscore", label: "my_name", errorMsg: "not a valid DNS label", expectError: true},
{name: "invalid dot", label: "my.name", errorMsg: "not a valid DNS label", expectError: true},
{name: "invalid starts with hyphen", label: "-name", errorMsg: "not a valid DNS label", expectError: true},
{name: "invalid ends with hyphen", label: "name-", errorMsg: "not a valid DNS label", expectError: true},
{name: "invalid too long", label: strings.Repeat("a", 64), errorMsg: "exceeds maximum length", expectError: true},
{name: "invalid space", label: "my name", errorMsg: "not a valid DNS label", expectError: true},
{name: "invalid special char", label: "name@", errorMsg: "not a valid DNS label", expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateDNS1123Label(tt.label, "test.field", "Test")
if tt.expectError {
assert.NotNil(t, err)
assert.Contains(t, err.Message, tt.errorMsg)
} else {
assert.Nil(t, err)
}
})
}
}
func TestValidateDNS1123Subdomain(t *testing.T) {
tests := []struct {
name string
subdomain string
errorMsg string
expectError bool
}{
{name: "valid simple", subdomain: "myname", errorMsg: "", expectError: false},
{name: "valid with hyphen", subdomain: "my-name", errorMsg: "", expectError: false},
{name: "valid with dot", subdomain: "my.name", errorMsg: "", expectError: false},
{name: "valid subdomain", subdomain: "app.example.com", errorMsg: "", expectError: false},
{name: "valid with numbers", subdomain: "app123.example456", errorMsg: "", expectError: false},
{name: "valid single char", subdomain: "a", errorMsg: "", expectError: false},
{name: "valid max length", subdomain: strings.Repeat("a", 253), errorMsg: "", expectError: false},
{name: "invalid empty", subdomain: "", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid uppercase", subdomain: "My.Name", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid underscore", subdomain: "my_name", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid starts with dot", subdomain: ".name", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid ends with dot", subdomain: "name.", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid double dot", subdomain: "my..name", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid starts with hyphen", subdomain: "-name", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid ends with hyphen", subdomain: "name-", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid too long", subdomain: strings.Repeat("a", 254), errorMsg: "exceeds maximum length", expectError: true},
{name: "invalid space", subdomain: "my name", errorMsg: "not a valid DNS subdomain", expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateDNS1123Subdomain(tt.subdomain, "test.field", "Test")
if tt.expectError {
assert.NotNil(t, err)
assert.Contains(t, err.Message, tt.errorMsg)
} else {
assert.Nil(t, err)
}
})
}
}
func TestValidator_ValidateSpecDurations(t *testing.T) {
validator := NewValidator()
tests := []struct {
config *Config
name string
errorContains []string
expectErrors bool
}{
{
name: "valid durations",
config: &Config{
HealthCheck: &HealthCheckSpec{
Interval: "5s",
Timeout: "2s",
MaxConnectionAge: "25m",
MaxIdleTime: "10m",
Method: "tcp-dial",
},
Reliability: &ReliabilitySpec{
TCPKeepalive: "30s",
DialTimeout: "30s",
WatchdogPeriod: "30s",
},
},
expectErrors: false,
},
{
name: "invalid health check interval",
config: &Config{
HealthCheck: &HealthCheckSpec{
Interval: "invalid",
},
},
expectErrors: true,
errorContains: []string{"Invalid health check interval"},
},
{
name: "invalid health check timeout",
config: &Config{
HealthCheck: &HealthCheckSpec{
Timeout: "abc",
},
},
expectErrors: true,
errorContains: []string{"Invalid health check timeout"},
},
{
name: "invalid max connection age",
config: &Config{
HealthCheck: &HealthCheckSpec{
MaxConnectionAge: "xyz",
},
},
expectErrors: true,
errorContains: []string{"Invalid max connection age"},
},
{
name: "invalid max idle time",
config: &Config{
HealthCheck: &HealthCheckSpec{
MaxIdleTime: "bad",
},
},
expectErrors: true,
errorContains: []string{"Invalid max idle time"},
},
{
name: "invalid health check method",
config: &Config{
HealthCheck: &HealthCheckSpec{
Method: "invalid-method",
},
},
expectErrors: true,
errorContains: []string{"Invalid health check method"},
},
{
name: "invalid TCP keepalive",
config: &Config{
Reliability: &ReliabilitySpec{
TCPKeepalive: "not-a-duration",
},
},
expectErrors: true,
errorContains: []string{"Invalid TCP keepalive duration"},
},
{
name: "invalid dial timeout",
config: &Config{
Reliability: &ReliabilitySpec{
DialTimeout: "bad",
},
},
expectErrors: true,
errorContains: []string{"Invalid dial timeout"},
},
{
name: "invalid watchdog period",
config: &Config{
Reliability: &ReliabilitySpec{
WatchdogPeriod: "invalid",
},
},
expectErrors: true,
errorContains: []string{"Invalid watchdog period"},
},
{
name: "multiple invalid durations",
config: &Config{
HealthCheck: &HealthCheckSpec{
Interval: "bad",
Timeout: "worse",
},
},
expectErrors: true,
errorContains: []string{"Invalid health check interval", "Invalid health check timeout"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.validateSpecDurations(tt.config)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found in errors: %v", expectedMsg, errs)
}
} else {
assert.Empty(t, errs, "expected no validation errors, got: %v", errs)
}
})
}
}
func TestValidator_ValidateContextAndNamespaceNames(t *testing.T) {
validator := NewValidator()
tests := []struct {
config *Config
name string
errorContains []string
expectErrors bool
}{
{
name: "valid context and namespace names",
config: &Config{
Contexts: []Context{
{
Name: "my-cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: false,
},
{
name: "valid context with underscores (kubeconfig style)",
config: &Config{
Contexts: []Context{
{
Name: "gke_project_zone_cluster",
Namespaces: []Namespace{
{
Name: "my-namespace",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: false, // Context names now allow underscores
},
{
name: "valid context with uppercase",
config: &Config{
Contexts: []Context{
{
Name: "MyCluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: false, // Context names now allow uppercase
},
{
name: "valid namespace with dots (subdomain style)",
config: &Config{
Contexts: []Context{
{
Name: "my-cluster",
Namespaces: []Namespace{
{
Name: "my.app.example",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: false, // Namespaces now allow dots (DNS subdomain format)
},
{
name: "invalid namespace name with uppercase",
config: &Config{
Contexts: []Context{
{
Name: "my-cluster",
Namespaces: []Namespace{
{
Name: "MyNamespace",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"not a valid DNS subdomain"},
},
{
name: "invalid context name with spaces",
config: &Config{
Contexts: []Context{
{
Name: "my cluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"not valid", "letter or digit"},
},
{
name: "context name too long",
config: &Config{
Contexts: []Context{
{
Name: strings.Repeat("a", 254),
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"exceeds maximum length"},
},
{
name: "invalid context name starts with hyphen",
config: &Config{
Contexts: []Context{
{
Name: "-mycluster",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"not valid", "letter or digit"},
},
{
name: "invalid context name ends with underscore",
config: &Config{
Contexts: []Context{
{
Name: "mycluster_",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"not valid", "letter or digit"},
},
{
name: "invalid namespace name with spaces",
config: &Config{
Contexts: []Context{
{
Name: "my-cluster",
Namespaces: []Namespace{
{
Name: "my namespace",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"not a valid DNS subdomain"},
},
{
name: "invalid namespace name with underscore",
config: &Config{
Contexts: []Context{
{
Name: "my-cluster",
Namespaces: []Namespace{
{
Name: "my_namespace",
Forwards: []Forward{{Resource: "pod/app", Port: 8080, LocalPort: 8080}},
},
},
},
},
},
expectErrors: true,
errorContains: []string{"not a valid DNS subdomain"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.ValidateConfig(tt.config)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found in errors: %v", expectedMsg, errs)
}
} else {
assert.Empty(t, errs, "expected no validation errors, got: %v", errs)
}
})
}
}
func TestValidator_ValidateResourceNames(t *testing.T) {
validator := NewValidator()
tests := []struct {
name string
errorContains []string
forward Forward
expectErrors bool
}{
{
name: "valid resource name",
forward: Forward{
Resource: "pod/my-app",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: false,
},
{
name: "valid resource name with subdomain",
forward: Forward{
Resource: "service/my-service.example.com",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: false,
},
{
name: "invalid resource name with uppercase",
forward: Forward{
Resource: "pod/MyApp",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"not a valid DNS subdomain"},
},
{
name: "invalid resource name with underscore",
forward: Forward{
Resource: "pod/my_app",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"not a valid DNS subdomain"},
},
{
name: "invalid resource name starts with hyphen",
forward: Forward{
Resource: "pod/-myapp",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: true,
errorContains: []string{"not a valid DNS subdomain"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.validateForward(&tt.forward)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found in errors: %v", expectedMsg, errs)
}
} else {
assert.Empty(t, errs, "expected no validation errors, got: %v", errs)
}
})
}
}
func TestIsValidResourceType(t *testing.T) {
tests := []struct {
resourceType string
expected bool
}{
{resourceType: "pod", expected: true},
{resourceType: "service", expected: true},
{resourceType: "deployment", expected: false},
{resourceType: "configmap", expected: false},
{resourceType: "", expected: false},
{resourceType: "POD", expected: false}, // case sensitive
{resourceType: "Pod", expected: false}, // case sensitive
}
for _, tt := range tests {
t.Run(tt.resourceType, func(t *testing.T) {
result := isValidResourceType(tt.resourceType)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsValidHealthCheckMethod(t *testing.T) {
tests := []struct {
method string
expected bool
}{
{method: "tcp-dial", expected: true},
{method: "data-transfer", expected: true},
{method: "ping", expected: false},
{method: "http", expected: false},
{method: "", expected: false},
{method: "TCP-DIAL", expected: false}, // case sensitive
}
for _, tt := range tests {
t.Run(tt.method, func(t *testing.T) {
result := isValidHealthCheckMethod(tt.method)
assert.Equal(t, tt.expected, result)
})
}
}
func TestValidator_ValidateHTTPLog(t *testing.T) {
validator := NewValidator()
tests := []struct {
name string
errorContains []string
forward Forward
expectErrors bool
}{
{
name: "valid HTTP log config",
forward: Forward{
Resource: "pod/app",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
HTTPLog: &HTTPLogSpec{
Enabled: true,
MaxBodySize: 1024,
LogFile: "/tmp/test.log",
},
},
expectErrors: false,
},
{
name: "HTTP log disabled - no validation needed",
forward: Forward{
Resource: "pod/app",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
HTTPLog: &HTTPLogSpec{
Enabled: false,
MaxBodySize: -1, // Would be invalid if enabled
},
},
expectErrors: false,
},
{
name: "no HTTP log config",
forward: Forward{
Resource: "pod/app",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
},
expectErrors: false,
},
{
name: "invalid negative maxBodySize",
forward: Forward{
Resource: "pod/app",
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
HTTPLog: &HTTPLogSpec{
Enabled: true,
MaxBodySize: -1,
},
},
expectErrors: true,
errorContains: []string{"maxBodySize", "non-negative"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.validateForward(&tt.forward)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
for _, expectedMsg := range tt.errorContains {
found := false
for _, err := range errs {
if strings.Contains(err.Message, expectedMsg) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found in errors: %v", expectedMsg, errs)
}
} else {
assert.Empty(t, errs, "expected no validation errors, got: %v", errs)
}
})
}
}
func TestValidateContextName(t *testing.T) {
tests := []struct {
name string
contextName string
errorMsg string
expectError bool
}{
// Valid cases
{name: "valid simple", contextName: "mycluster", errorMsg: "", expectError: false},
{name: "valid with hyphen", contextName: "my-cluster", errorMsg: "", expectError: false},
{name: "valid with underscore", contextName: "my_cluster", errorMsg: "", expectError: false},
{name: "valid with numbers", contextName: "cluster123", errorMsg: "", expectError: false},
{name: "valid mixed", contextName: "my-cluster_123", errorMsg: "", expectError: false},
{name: "valid uppercase", contextName: "MyCluster", errorMsg: "", expectError: false},
{name: "valid mixed case", contextName: "myCluster-Test_123", errorMsg: "", expectError: false},
{name: "valid GKE style", contextName: "gke_project_us-central1_cluster", errorMsg: "", expectError: false},
{name: "valid minikube", contextName: "minikube", errorMsg: "", expectError: false},
{name: "valid docker desktop", contextName: "docker-desktop", errorMsg: "", expectError: false},
{name: "valid docker desktop alt", contextName: "docker_desktop", errorMsg: "", expectError: false},
{name: "valid single char", contextName: "a", errorMsg: "", expectError: false},
{name: "valid single digit", contextName: "1", errorMsg: "", expectError: false},
{name: "valid starts with digit", contextName: "123-cluster", errorMsg: "", expectError: false},
{name: "valid user@cluster", contextName: "admin@home", errorMsg: "", expectError: false},
{name: "valid user@fqdn", contextName: "user@cluster.example.com", errorMsg: "", expectError: false},
{name: "valid dotted FQDN", contextName: "cluster.example.com", errorMsg: "", expectError: false},
{name: "valid GKE dotted", contextName: "gke_proj_zone_cluster.prod", errorMsg: "", expectError: false},
{name: "valid EKS ARN", contextName: "arn:aws:eks:us-east-1:123:cluster/foo", errorMsg: "", expectError: false},
// Invalid cases
{name: "invalid empty", contextName: "", errorMsg: "not valid", expectError: true},
{name: "invalid starts with hyphen", contextName: "-cluster", errorMsg: "not valid", expectError: true},
{name: "invalid ends with hyphen", contextName: "cluster-", errorMsg: "not valid", expectError: true},
{name: "invalid starts with underscore", contextName: "_cluster", errorMsg: "not valid", expectError: true},
{name: "invalid ends with underscore", contextName: "cluster_", errorMsg: "not valid", expectError: true},
{name: "invalid starts with @", contextName: "@cluster", errorMsg: "not valid", expectError: true},
{name: "invalid ends with /", contextName: "cluster/", errorMsg: "not valid", expectError: true},
{name: "invalid ends with .", contextName: "cluster.", errorMsg: "not valid", expectError: true},
{name: "invalid with spaces", contextName: "my cluster", errorMsg: "not valid", expectError: true},
{name: "invalid too long", contextName: strings.Repeat("a", 254), errorMsg: "exceeds maximum length", expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateContextName(tt.contextName, "test.field")
if tt.expectError {
assert.NotNil(t, err)
assert.Contains(t, err.Message, tt.errorMsg)
} else {
assert.Nil(t, err)
}
})
}
}
func TestValidateNamespaceName(t *testing.T) {
tests := []struct {
name string
namespace string
errorMsg string
expectError bool
}{
// Valid cases
{name: "valid simple", namespace: "default", errorMsg: "", expectError: false},
{name: "valid with hyphen", namespace: "kube-system", errorMsg: "", expectError: false},
{name: "valid with dots", namespace: "my.app.example", errorMsg: "", expectError: false},
{name: "valid subdomain", namespace: "app.ns.cluster.local", errorMsg: "", expectError: false},
{name: "valid with numbers", namespace: "ns123", errorMsg: "", expectError: false},
{name: "valid mixed", namespace: "my-app-123.test", errorMsg: "", expectError: false},
{name: "valid single char", namespace: "a", errorMsg: "", expectError: false},
{name: "valid single digit", namespace: "1", errorMsg: "", expectError: false},
{name: "valid starts with digit", namespace: "123-ns", errorMsg: "", expectError: false},
// Invalid cases
{name: "invalid empty", namespace: "", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid starts with hyphen", namespace: "-namespace", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid ends with hyphen", namespace: "namespace-", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid with underscore", namespace: "my_namespace", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid with spaces", namespace: "my namespace", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid starts with dot", namespace: ".namespace", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid ends with dot", namespace: "namespace.", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid double dot", namespace: "my..namespace", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid uppercase", namespace: "MyNamespace", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid with special chars", namespace: "ns@123", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid with slash", namespace: "ns/name", errorMsg: "not a valid DNS subdomain", expectError: true},
{name: "invalid too long", namespace: strings.Repeat("a", 254), errorMsg: "exceeds maximum length", expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateNamespaceName(tt.namespace, "test.field")
if tt.expectError {
assert.NotNil(t, err)
assert.Contains(t, err.Message, tt.errorMsg)
} else {
assert.Nil(t, err)
}
})
}
}
func TestIsValidPort(t *testing.T) {
tests := []struct {
name string
port int
expected bool
}{
// Valid ports
{name: "valid minimum", port: 1, expected: true},
{name: "valid maximum", port: 65535, expected: true},
{name: "valid common", port: 8080, expected: true},
{name: "valid HTTP", port: 80, expected: true},
{name: "valid HTTPS", port: 443, expected: true},
{name: "valid high", port: 30000, expected: true},
// Invalid ports
{name: "invalid zero", port: 0, expected: false},
{name: "invalid negative", port: -1, expected: false},
{name: "invalid too high", port: 65536, expected: false},
{name: "invalid very high", port: 100000, expected: false},
{name: "invalid negative large", port: -8080, expected: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsValidPort(tt.port)
assert.Equal(t, tt.expected, result, "IsValidPort(%d) = %v, want %v", tt.port, result, tt.expected)
})
}
}
func TestValidateProtocol(t *testing.T) {
validator := NewValidator()
tests := []struct {
name string
protocol string
errorContains string
expectErrors bool
}{
// Valid protocols
{name: "valid tcp", protocol: "tcp", errorContains: "", expectErrors: false},
{name: "valid empty", protocol: "", errorContains: "", expectErrors: false},
// Invalid protocols
{name: "invalid udp", protocol: "udp", errorContains: "only 'tcp' is supported", expectErrors: true},
{name: "invalid http", protocol: "http", errorContains: "only 'tcp' is supported", expectErrors: true},
{name: "invalid https", protocol: "https", errorContains: "only 'tcp' is supported", expectErrors: true},
{name: "invalid uppercase TCP", protocol: "TCP", errorContains: "only 'tcp' is supported", expectErrors: true},
{name: "invalid mixed case", protocol: "Tcp", errorContains: "only 'tcp' is supported", expectErrors: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fwd := Forward{
Resource: "pod/my-app",
Protocol: tt.protocol,
Port: 8080,
LocalPort: 8080,
contextName: "dev",
namespaceName: "default",
}
errs := validator.validateForward(&fwd)
if tt.expectErrors {
assert.NotEmpty(t, errs, "expected validation errors")
found := false
for _, err := range errs {
if strings.Contains(err.Message, tt.errorContains) {
found = true
break
}
}
assert.True(t, found, "expected error message '%s' not found in errors: %v", tt.errorContains, errs)
} else {
assert.Empty(t, errs, "expected no validation errors, got: %v", errs)
}
})
}
}