mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-09 23:59:45 +00:00
2048 lines
55 KiB
Go
2048 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", "alphanumeric"},
|
|
},
|
|
{
|
|
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", "start/end with alphanumeric"},
|
|
},
|
|
{
|
|
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", "start/end with alphanumeric"},
|
|
},
|
|
{
|
|
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},
|
|
|
|
// 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 with spaces", contextName: "my cluster", errorMsg: "not valid", expectError: true},
|
|
{name: "invalid with dots", contextName: "my.cluster", errorMsg: "not valid", expectError: true},
|
|
{name: "invalid with special chars", contextName: "cluster@123", errorMsg: "not valid", expectError: true},
|
|
{name: "invalid with slash", contextName: "cluster/name", 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)
|
|
}
|
|
})
|
|
}
|
|
}
|