Files
kportal/internal/config/config_test.go
T
lukaszraczylo 96ae1d45e0 style: Extract UI constants and refactor main view rendering (#30)
- [x] Add golangci-lint configuration with gocritic ifElseChain disabled
- [x] Rename error variables to avoid shadowing (createErr, watcherErr, watchErr, etc.)
- [x] Replace `interface{}` with `any` type alias throughout codebase
- [x] Add package-level documentation comments to all internal packages
- [x] Reorder struct fields alphabetically for consistency
- [x] Extract UI constants (terminal dimensions, column widths, colors) to constants.go
- [x] Refactor BubbleTeaUI main view rendering into smaller helper functions
- [x] Simplify nested conditionals and improve code clarity
- [x] Add `isForwardDisabled()` helper method to BubbleTeaUI
- [x] Update file permissions from 0644 to 0600 in config tests
- [x] Add `#nosec` comments and error suppression where appropriate
- [x] Improve test table struct field ordering for readability
- [x] Fix resource parsing in AddForward using strings.SplitN
- [x] Add comprehensive tests for new UI helper functions and constants
2026-01-13 09:37:45 +00:00

518 lines
13 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadConfig_ValidYAML(t *testing.T) {
// Create a temporary config file
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
validYAML := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/my-app
protocol: tcp
port: 8080
localPort: 8080
- name: staging
forwards:
- resource: service/postgres
protocol: tcp
port: 5432
localPort: 5433
- name: prod-cluster
namespaces:
- name: production
forwards:
- resource: pod
selector: app=nginx,env=prod
protocol: tcp
port: 80
localPort: 8081
`
err := os.WriteFile(configPath, []byte(validYAML), 0600)
assert.NoError(t, err, "should write temp config file")
// Load the config
cfg, err := LoadConfig(configPath)
assert.NoError(t, err, "LoadConfig should succeed")
assert.NotNil(t, cfg, "config should not be nil")
// Verify structure
assert.Len(t, cfg.Contexts, 2, "should have 2 contexts")
// Verify first context
assert.Equal(t, "dev-cluster", cfg.Contexts[0].Name)
assert.Len(t, cfg.Contexts[0].Namespaces, 2, "dev-cluster should have 2 namespaces")
// Verify first namespace in first context
assert.Equal(t, "default", cfg.Contexts[0].Namespaces[0].Name)
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
// Verify forward details
fwd := cfg.Contexts[0].Namespaces[0].Forwards[0]
assert.Equal(t, "pod/my-app", fwd.Resource)
assert.Equal(t, "tcp", fwd.Protocol)
assert.Equal(t, 8080, fwd.Port)
assert.Equal(t, 8080, fwd.LocalPort)
assert.Equal(t, "", fwd.Selector)
// Verify runtime fields are populated
assert.Equal(t, "dev-cluster", fwd.GetContext())
assert.Equal(t, "default", fwd.GetNamespace())
}
func TestLoadConfig_InvalidYAML(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
invalidYAML := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards: [this is invalid yaml syntax
`
err := os.WriteFile(configPath, []byte(invalidYAML), 0600)
assert.NoError(t, err, "should write temp config file")
// Load the config
cfg, err := LoadConfig(configPath)
assert.Error(t, err, "LoadConfig should fail with invalid YAML")
assert.Nil(t, cfg, "config should be nil on error")
assert.Contains(t, err.Error(), "failed to parse YAML", "error should mention YAML parsing")
}
func TestLoadConfig_FileNotFound(t *testing.T) {
// Try to load a non-existent file
cfg, err := LoadConfig("/non/existent/path/.kportal.yaml")
assert.Error(t, err, "LoadConfig should fail with non-existent file")
assert.Nil(t, cfg, "config should be nil on error")
assert.Equal(t, ErrConfigNotFound, err, "should return ErrConfigNotFound")
}
func TestForward_ID(t *testing.T) {
tests := []struct {
name string
expectedID string
forward Forward
}{
{
name: "pod with explicit name",
forward: Forward{
Resource: "pod/my-app",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
expectedID: "dev-cluster/default/pod/my-app:8080",
},
{
name: "service resource",
forward: Forward{
Resource: "service/postgres",
Port: 5432,
LocalPort: 5433,
contextName: "prod-cluster",
namespaceName: "database",
},
expectedID: "prod-cluster/database/service/postgres:5433",
},
{
name: "pod with selector",
forward: Forward{
Resource: "pod",
Selector: "app=nginx",
Port: 80,
LocalPort: 8081,
contextName: "staging",
namespaceName: "web",
},
expectedID: "staging/web/pod:8081",
},
{
name: "forward with alias",
forward: Forward{
Resource: "service/postgres",
Port: 5432,
LocalPort: 5432,
Alias: "shared-postgres",
contextName: "home",
namespaceName: "shared-resources",
},
expectedID: "shared-postgres:5432",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id := tt.forward.ID()
assert.Equal(t, tt.expectedID, id, "ID() should return correct format")
})
}
}
func TestForward_String(t *testing.T) {
tests := []struct {
name string
expectedString string
forward Forward
}{
{
name: "pod without selector",
forward: Forward{
Resource: "pod/my-app",
Port: 8080,
LocalPort: 8080,
contextName: "dev-cluster",
namespaceName: "default",
},
expectedString: "dev-cluster/default/pod/my-app:8080→8080",
},
{
name: "service resource",
forward: Forward{
Resource: "service/postgres",
Port: 5432,
LocalPort: 5433,
contextName: "prod-cluster",
namespaceName: "database",
},
expectedString: "prod-cluster/database/service/postgres:5432→5433",
},
{
name: "pod with selector",
forward: Forward{
Resource: "pod",
Selector: "app=nginx,env=prod",
Port: 80,
LocalPort: 8081,
contextName: "staging",
namespaceName: "web",
},
expectedString: "staging/web/pod[app=nginx,env=prod]:80→8081",
},
{
name: "forward with alias",
forward: Forward{
Resource: "service/redis",
Port: 6379,
LocalPort: 6379,
Alias: "redis-at-home",
contextName: "home",
namespaceName: "shared-resources",
},
expectedString: "redis-at-home:6379→6379",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
str := tt.forward.String()
assert.Equal(t, tt.expectedString, str, "String() should return correct format")
})
}
}
func TestParseConfig_ValidYAML(t *testing.T) {
yamlData := []byte(`contexts:
- name: test-cluster
namespaces:
- name: default
forwards:
- resource: pod/app
protocol: tcp
port: 8080
localPort: 8080
`)
cfg, err := ParseConfig(yamlData)
assert.NoError(t, err, "ParseConfig should succeed")
assert.NotNil(t, cfg, "config should not be nil")
assert.Len(t, cfg.Contexts, 1)
assert.Equal(t, "test-cluster", cfg.Contexts[0].Name)
}
func TestParseConfig_PopulatesRuntimeFields(t *testing.T) {
yamlData := []byte(`contexts:
- name: my-cluster
namespaces:
- name: my-namespace
forwards:
- resource: pod/my-pod
port: 8080
localPort: 8080
`)
cfg, err := ParseConfig(yamlData)
assert.NoError(t, err)
assert.NotNil(t, cfg)
// Check that runtime fields are populated
fwd := cfg.Contexts[0].Namespaces[0].Forwards[0]
assert.Equal(t, "my-cluster", fwd.GetContext())
assert.Equal(t, "my-namespace", fwd.GetNamespace())
assert.Equal(t, "my-cluster/my-namespace/pod/my-pod:8080", fwd.ID())
}
func TestConfig_GetAllForwards(t *testing.T) {
yamlData := []byte(`contexts:
- name: cluster1
namespaces:
- name: ns1
forwards:
- resource: pod/app1
port: 8080
localPort: 8080
- resource: pod/app2
port: 8081
localPort: 8081
- name: ns2
forwards:
- resource: service/db
port: 5432
localPort: 5432
- name: cluster2
namespaces:
- name: ns3
forwards:
- resource: pod/app3
port: 9090
localPort: 9090
`)
cfg, err := ParseConfig(yamlData)
assert.NoError(t, err)
forwards := cfg.GetAllForwards()
assert.Len(t, forwards, 4, "should return all forwards from all contexts and namespaces")
}
func TestForward_SetContext(t *testing.T) {
fwd := Forward{
Resource: "pod/my-app",
Port: 8080,
LocalPort: 8080,
}
assert.Equal(t, "", fwd.GetContext(), "initial context should be empty")
assert.Equal(t, "", fwd.GetNamespace(), "initial namespace should be empty")
fwd.SetContext("my-cluster", "my-namespace")
assert.Equal(t, "my-cluster", fwd.GetContext())
assert.Equal(t, "my-namespace", fwd.GetNamespace())
}
func TestHTTPLogSpec_UnmarshalYAML(t *testing.T) {
tests := []struct {
name string
yaml string
expected bool
}{
{
name: "httpLog as boolean true",
yaml: `contexts:
- name: test
namespaces:
- name: default
forwards:
- resource: service/api
port: 8080
localPort: 8080
httpLog: true
`,
expected: true,
},
{
name: "httpLog as boolean false",
yaml: `contexts:
- name: test
namespaces:
- name: default
forwards:
- resource: service/api
port: 8080
localPort: 8080
httpLog: false
`,
expected: false,
},
{
name: "httpLog as struct",
yaml: `contexts:
- name: test
namespaces:
- name: default
forwards:
- resource: service/api
port: 8080
localPort: 8080
httpLog:
enabled: true
includeHeaders: true
`,
expected: true,
},
{
name: "httpLog not specified",
yaml: `contexts:
- name: test
namespaces:
- name: default
forwards:
- resource: service/api
port: 8080
localPort: 8080
`,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := ParseConfig([]byte(tt.yaml))
assert.NoError(t, err)
assert.NotNil(t, cfg)
fwd := cfg.Contexts[0].Namespaces[0].Forwards[0]
if tt.expected {
assert.NotNil(t, fwd.HTTPLog, "HTTPLog should not be nil")
assert.True(t, fwd.HTTPLog.Enabled, "HTTPLog.Enabled should be true")
} else if fwd.HTTPLog != nil {
assert.False(t, fwd.HTTPLog.Enabled, "HTTPLog.Enabled should be false")
}
})
}
}
func TestNewEmptyConfig(t *testing.T) {
cfg := NewEmptyConfig()
assert.NotNil(t, cfg, "NewEmptyConfig should return non-nil config")
assert.Empty(t, cfg.Contexts, "NewEmptyConfig should have empty contexts")
assert.True(t, cfg.IsEmpty(), "NewEmptyConfig should be considered empty")
}
func TestConfig_IsEmpty(t *testing.T) {
tests := []struct {
config *Config
name string
expected bool
}{
{
name: "nil contexts",
config: &Config{},
expected: true,
},
{
name: "empty contexts slice",
config: &Config{Contexts: []Context{}},
expected: true,
},
{
name: "context with empty namespaces",
config: &Config{
Contexts: []Context{
{Name: "test", Namespaces: []Namespace{}},
},
},
expected: true,
},
{
name: "context with namespace but no forwards",
config: &Config{
Contexts: []Context{
{
Name: "test",
Namespaces: []Namespace{
{Name: "default", Forwards: []Forward{}},
},
},
},
},
expected: true,
},
{
name: "config with forward",
config: &Config{
Contexts: []Context{
{
Name: "test",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app", Port: 8080, LocalPort: 8080},
},
},
},
},
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.config.IsEmpty())
})
}
}
func TestCreateEmptyConfigFile(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create empty config file
err := CreateEmptyConfigFile(configPath)
assert.NoError(t, err, "CreateEmptyConfigFile should succeed")
// Verify file exists
_, err = os.Stat(configPath)
assert.NoError(t, err, "config file should exist")
// Verify file is readable and parseable
cfg, err := LoadConfig(configPath)
assert.NoError(t, err, "should be able to load created config")
assert.NotNil(t, cfg, "config should not be nil")
assert.True(t, cfg.IsEmpty(), "created config should be empty")
// Verify file permissions (0600)
info, _ := os.Stat(configPath)
assert.Equal(t, os.FileMode(0600), info.Mode().Perm(), "file should have 0600 permissions")
// Verify file contains helpful header
content, _ := os.ReadFile(configPath)
assert.Contains(t, string(content), "# kportal configuration file", "should contain header comment")
assert.Contains(t, string(content), "Example forward", "should contain example")
}
func TestCreateEmptyConfigFile_AlreadyExists(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create existing file
err := os.WriteFile(configPath, []byte("existing content"), 0600)
assert.NoError(t, err)
// Try to create config file - should fail
err = CreateEmptyConfigFile(configPath)
assert.Error(t, err, "CreateEmptyConfigFile should fail when file exists")
assert.Contains(t, err.Error(), "already exists")
// Verify original content is preserved
content, _ := os.ReadFile(configPath)
assert.Equal(t, "existing content", string(content))
}