mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-11 00:09:31 +00:00
96ae1d45e0
- [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
518 lines
13 KiB
Go
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))
|
|
}
|