Files
kportal/internal/config/validator_test.go
T
f3d3f259a1 Allow configuring forwards-table columns, order, and width (#82)
* Allow configuring forwards-table columns, order, and width

Adds an optional tui.columns section to .kportal.yaml so the visible
columns, their order, and their widths can be customized instead of
always rendering the hardcoded 8-column layout. Omitting the section
keeps the existing default columns/order/widths.

Closes #81

* fix(ui): keep the plain table layout and check column widths

The plain verbose table must look the same when no tui section is
set. It had narrower columns and shorter port headers than before. The
default layout is now identical to the old one, and a golden test
guards it.

A status column that is not last now pads by visible width. Table
rules follow the column widths. The UI applies new columns only after a
reload succeeds, including the SIGHUP path of the verbose table.

The validator rejects a column width above 200. The README describes
the tui.columns setting.

Refs #81

* fix(httplog): replace the deprecated ReverseProxy Director with Rewrite

Staticcheck reports SA1019 because Go 1.26 deprecates Director. The
proxy now sets the target in a Rewrite function. Rewrite drops the
X-Forwarded-For header that Director sent, so the function calls
SetXForwarded to keep that header.

A new test checks the target port, the path and the header.

* fix(release): drop the deprecated cask url verified key

Homebrew warns about the verified parameter of the cask url stanza.
The setting is not needed. GoReleaser check now passes without a
deprecation warning.

The postflight warning remains. GoReleaser generates it from the
post install hook, and the upstream fix is not released yet.

Refs #79

---------

Co-authored-by: Lukasz Raczylo <[email protected]>
2026-10-02 02:24:11 +01:00

2187 lines
58 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: "[email protected]", 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)
}
})
}
}
func TestValidator_ValidateTUI(t *testing.T) {
validator := NewValidator()
baseContexts := []Context{
{
Name: "dev",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app", Protocol: "tcp", Port: 8080, LocalPort: 8080, contextName: "dev", namespaceName: "default"},
},
},
},
},
}
tests := []struct {
config *Config
name string
errorContains string
expectErrors bool
}{
{
name: "no tui config - no validation",
config: &Config{Contexts: baseContexts},
expectErrors: false,
},
{
name: "valid columns",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{
Columns: []TableColumn{
{Name: "alias", Width: 30},
{Name: "status"},
},
},
},
expectErrors: false,
},
{
name: "unknown column name",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{Columns: []TableColumn{{Name: "bogus"}}},
},
expectErrors: true,
errorContains: "Invalid column name",
},
{
name: "empty column name",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{Columns: []TableColumn{{Name: ""}}},
},
expectErrors: true,
errorContains: "cannot be empty",
},
{
name: "duplicate column",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{Columns: []TableColumn{{Name: "alias"}, {Name: "Alias"}}},
},
expectErrors: true,
errorContains: "Duplicate column",
},
{
name: "negative width",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{Columns: []TableColumn{{Name: "alias", Width: -1}}},
},
expectErrors: true,
errorContains: "cannot be negative",
},
{
name: "width at cap",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{Columns: []TableColumn{{Name: "alias", Width: 200}}},
},
expectErrors: false,
},
{
name: "width above cap",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{Columns: []TableColumn{{Name: "alias", Width: 201}}},
},
expectErrors: true,
errorContains: "cannot exceed 200",
},
{
name: "name is case and whitespace insensitive",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{Columns: []TableColumn{{Name: " Alias "}, {Name: "STATUS"}}},
},
expectErrors: false,
},
{
name: "padded duplicate is detected",
config: &Config{
Contexts: baseContexts,
TUI: &TUISpec{Columns: []TableColumn{{Name: "alias"}, {Name: " Alias "}}},
},
expectErrors: true,
errorContains: "Duplicate column",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validator.ValidateConfig(tt.config)
if tt.expectErrors {
assert.NotEmpty(t, errs)
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)
}
})
}
}