Files
kportal/internal/config/mutator_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

665 lines
17 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestNewMutator tests mutator creation
func TestNewMutator(t *testing.T) {
mutator := NewMutator("/path/to/config.yaml")
assert.NotNil(t, mutator)
assert.Equal(t, "/path/to/config.yaml", mutator.configPath)
}
// TestMutator_AddForward_NewFile tests adding a forward to a new file
// Note: Due to how LoadConfig wraps errors, os.IsNotExist check in AddForward
// doesn't work with wrapped errors. This documents the current behavior.
func TestMutator_AddForward_NewFile(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
mutator := NewMutator(configPath)
fwd := Forward{
Resource: "pod/my-app",
Protocol: "tcp",
Port: 8080,
LocalPort: 8080,
}
// Currently fails because LoadConfig wraps the error and os.IsNotExist doesn't match
err := mutator.AddForward("dev-cluster", "default", fwd)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to load config")
}
// TestMutator_AddForward_EmptyFile tests adding a forward to an empty file
func TestMutator_AddForward_EmptyFile(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create empty config file with minimal valid structure
initial := `contexts: []
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
fwd := Forward{
Resource: "pod/my-app",
Protocol: "tcp",
Port: 8080,
LocalPort: 8080,
}
err = mutator.AddForward("dev-cluster", "default", fwd)
require.NoError(t, err)
// Verify file was updated and contains the forward
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
require.NotNil(t, cfg)
assert.Len(t, cfg.Contexts, 1)
assert.Equal(t, "dev-cluster", cfg.Contexts[0].Name)
assert.Len(t, cfg.Contexts[0].Namespaces, 1)
assert.Equal(t, "default", cfg.Contexts[0].Namespaces[0].Name)
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
assert.Equal(t, "pod/my-app", cfg.Contexts[0].Namespaces[0].Forwards[0].Resource)
}
// TestMutator_AddForward_ExistingFile tests adding to existing config
func TestMutator_AddForward_ExistingFile(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/existing-app
protocol: tcp
port: 8080
localPort: 8080
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
fwd := Forward{
Resource: "service/postgres",
Protocol: "tcp",
Port: 5432,
LocalPort: 5432,
}
err = mutator.AddForward("dev-cluster", "default", fwd)
require.NoError(t, err)
// Verify both forwards exist
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 2)
}
// TestMutator_AddForward_NewContext tests adding to new context
func TestMutator_AddForward_NewContext(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/app
protocol: tcp
port: 8080
localPort: 8080
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
fwd := Forward{
Resource: "pod/prod-app",
Protocol: "tcp",
Port: 80,
LocalPort: 8081,
}
err = mutator.AddForward("prod-cluster", "production", fwd)
require.NoError(t, err)
// Verify new context was created
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
assert.Len(t, cfg.Contexts, 2)
assert.Equal(t, "prod-cluster", cfg.Contexts[1].Name)
}
// TestMutator_AddForward_DuplicatePort tests rejecting duplicate ports
func TestMutator_AddForward_DuplicatePort(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/app
protocol: tcp
port: 8080
localPort: 8080
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
fwd := Forward{
Resource: "pod/another-app",
Protocol: "tcp",
Port: 9090,
LocalPort: 8080, // Duplicate local port
}
err = mutator.AddForward("dev-cluster", "default", fwd)
assert.Error(t, err)
assert.Contains(t, err.Error(), "port 8080 is already in use")
}
// TestMutator_AddForward_InvalidForward tests rejecting invalid forward
func TestMutator_AddForward_InvalidForward(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config file
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/existing-app
protocol: tcp
port: 8080
localPort: 8080
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
fwd := Forward{
Resource: "invalid/type/resource", // Invalid resource
Protocol: "tcp",
Port: 9090,
LocalPort: 9090,
}
err = mutator.AddForward("dev-cluster", "default", fwd)
assert.Error(t, err)
assert.Contains(t, err.Error(), "validation failed")
}
// TestMutator_RemoveForwards tests removing forwards by predicate
func TestMutator_RemoveForwards(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config with multiple forwards
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/app1
protocol: tcp
port: 8080
localPort: 8080
- resource: pod/app2
protocol: tcp
port: 8081
localPort: 8081
- resource: service/postgres
protocol: tcp
port: 5432
localPort: 5432
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
// Remove all pod resources
err = mutator.RemoveForwards(func(ctx, ns string, fwd Forward) bool {
return fwd.Resource == "pod/app1"
})
require.NoError(t, err)
// Verify the forward was removed
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 2)
for _, fwd := range cfg.Contexts[0].Namespaces[0].Forwards {
assert.NotEqual(t, "pod/app1", fwd.Resource)
}
}
// TestMutator_RemoveForwards_RemovesEmptyNamespaces tests that empty namespaces are removed
func TestMutator_RemoveForwards_RemovesEmptyNamespaces(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create config with two namespaces
initial := `contexts:
- name: dev-cluster
namespaces:
- name: ns1
forwards:
- resource: pod/app1
protocol: tcp
port: 8080
localPort: 8080
- name: ns2
forwards:
- resource: pod/app2
protocol: tcp
port: 8081
localPort: 8081
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
// Remove all forwards from ns1
err = mutator.RemoveForwards(func(ctx, ns string, fwd Forward) bool {
return ns == "ns1"
})
require.NoError(t, err)
// Verify ns1 was removed (has no forwards left)
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
assert.Len(t, cfg.Contexts[0].Namespaces, 1)
assert.Equal(t, "ns2", cfg.Contexts[0].Namespaces[0].Name)
}
// TestMutator_RemoveForwards_NonExistentFile tests removing from non-existent file
func TestMutator_RemoveForwards_NonExistentFile(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
mutator := NewMutator(configPath)
err := mutator.RemoveForwards(func(ctx, ns string, fwd Forward) bool {
return true
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to load config")
}
// TestMutator_RemoveForwardByID tests removing a specific forward by ID
func TestMutator_RemoveForwardByID(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/app1
protocol: tcp
port: 8080
localPort: 8080
- resource: pod/app2
protocol: tcp
port: 8081
localPort: 8081
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
// Remove by ID
err = mutator.RemoveForwardByID("dev-cluster/default/pod/app1:8080")
require.NoError(t, err)
// Verify the forward was removed
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
assert.Equal(t, "pod/app2", cfg.Contexts[0].Namespaces[0].Forwards[0].Resource)
}
// TestMutator_UpdateForward tests updating an existing forward
func TestMutator_UpdateForward(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/app1
protocol: tcp
port: 8080
localPort: 8080
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
newFwd := Forward{
Resource: "pod/app1-updated",
Protocol: "tcp",
Port: 9090,
LocalPort: 9090,
}
err = mutator.UpdateForward("dev-cluster/default/pod/app1:8080", "dev-cluster", "default", newFwd)
require.NoError(t, err)
// Verify the forward was updated
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
assert.Equal(t, "pod/app1-updated", cfg.Contexts[0].Namespaces[0].Forwards[0].Resource)
assert.Equal(t, 9090, cfg.Contexts[0].Namespaces[0].Forwards[0].LocalPort)
}
// TestMutator_UpdateForward_MoveToNewContext tests moving forward to new context
func TestMutator_UpdateForward_MoveToNewContext(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config with multiple forwards (so removing one doesn't leave empty namespace)
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/app1
protocol: tcp
port: 8080
localPort: 8080
- resource: pod/app2
protocol: tcp
port: 9090
localPort: 9090
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
newFwd := Forward{
Resource: "pod/app1-moved",
Protocol: "tcp",
Port: 8080,
LocalPort: 8080,
}
err = mutator.UpdateForward("dev-cluster/default/pod/app1:8080", "prod-cluster", "production", newFwd)
require.NoError(t, err)
// Verify the forward was moved
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
// New context should exist with the forward
assert.Len(t, cfg.Contexts, 2)
// Original namespace should still have one forward
assert.Len(t, cfg.Contexts[0].Namespaces, 1)
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
// New context should have the moved forward
assert.Equal(t, "prod-cluster", cfg.Contexts[1].Name)
assert.Len(t, cfg.Contexts[1].Namespaces, 1)
assert.Equal(t, "production", cfg.Contexts[1].Namespaces[0].Name)
}
// TestMutator_UpdateForward_NotFound tests updating non-existent forward
func TestMutator_UpdateForward_NotFound(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/app
protocol: tcp
port: 8080
localPort: 8080
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
newFwd := Forward{
Resource: "pod/app",
Protocol: "tcp",
Port: 8080,
LocalPort: 8080,
}
err = mutator.UpdateForward("non-existent-id", "dev-cluster", "default", newFwd)
assert.Error(t, err)
assert.Contains(t, err.Error(), "forward with ID non-existent-id not found")
}
// TestMutator_UpdateForward_DuplicatePort tests rejecting update with duplicate port
func TestMutator_UpdateForward_DuplicatePort(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config with two forwards
initial := `contexts:
- name: dev-cluster
namespaces:
- name: default
forwards:
- resource: pod/app1
protocol: tcp
port: 8080
localPort: 8080
- resource: pod/app2
protocol: tcp
port: 9090
localPort: 9090
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
// Try to update app1 to use the same port as app2
newFwd := Forward{
Resource: "pod/app1-updated",
Protocol: "tcp",
Port: 9090,
LocalPort: 9090, // Duplicate with app2
}
err = mutator.UpdateForward("dev-cluster/default/pod/app1:8080", "dev-cluster", "default", newFwd)
assert.Error(t, err)
assert.Contains(t, err.Error(), "port 9090 is already in use")
}
// TestMutator_WriteAtomic tests atomic write behavior
func TestMutator_WriteAtomic(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
mutator := NewMutator(configPath)
cfg := &Config{
Contexts: []Context{
{
Name: "test",
Namespaces: []Namespace{
{
Name: "default",
Forwards: []Forward{
{Resource: "pod/app", Protocol: "tcp", Port: 8080, LocalPort: 8080},
},
},
},
},
},
}
err := mutator.writeAtomic(cfg)
require.NoError(t, err)
// Verify file was created with correct permissions
info, err := os.Stat(configPath)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0600), info.Mode().Perm())
// Verify temp file was cleaned up
tmpFile := filepath.Join(tmpDir, ".kportal.yaml.tmp")
_, err = os.Stat(tmpFile)
assert.True(t, os.IsNotExist(err))
}
// TestMutator_FindOrCreateContext tests context finding/creation
func TestMutator_FindOrCreateContext(t *testing.T) {
mutator := NewMutator("/fake/path")
t.Run("find existing context", func(t *testing.T) {
cfg := &Config{
Contexts: []Context{
{Name: "existing"},
},
}
ctx := mutator.findOrCreateContext(cfg, "existing")
assert.Equal(t, "existing", ctx.Name)
assert.Len(t, cfg.Contexts, 1)
})
t.Run("create new context", func(t *testing.T) {
cfg := &Config{
Contexts: []Context{
{Name: "existing"},
},
}
ctx := mutator.findOrCreateContext(cfg, "new-context")
assert.Equal(t, "new-context", ctx.Name)
assert.Len(t, cfg.Contexts, 2)
})
}
// TestMutator_FindOrCreateNamespace tests namespace finding/creation
func TestMutator_FindOrCreateNamespace(t *testing.T) {
mutator := NewMutator("/fake/path")
t.Run("find existing namespace", func(t *testing.T) {
ctx := &Context{
Name: "test",
Namespaces: []Namespace{
{Name: "existing"},
},
}
ns := mutator.findOrCreateNamespace(ctx, "existing")
assert.Equal(t, "existing", ns.Name)
assert.Len(t, ctx.Namespaces, 1)
})
t.Run("create new namespace", func(t *testing.T) {
ctx := &Context{
Name: "test",
Namespaces: []Namespace{
{Name: "existing"},
},
}
ns := mutator.findOrCreateNamespace(ctx, "new-namespace")
assert.Equal(t, "new-namespace", ns.Name)
assert.Len(t, ctx.Namespaces, 2)
})
}
// TestMutator_Concurrent tests mutex protection
func TestMutator_Concurrent(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".kportal.yaml")
// Create initial config file
initial := `contexts:
- name: dev
namespaces:
- name: default
forwards:
- resource: pod/app
protocol: tcp
port: 8080
localPort: 8080
`
err := os.WriteFile(configPath, []byte(initial), 0600)
require.NoError(t, err)
mutator := NewMutator(configPath)
// Run concurrent operations
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func(port int) {
defer func() { done <- true }()
fwd := Forward{
Resource: "pod/app",
Protocol: "tcp",
Port: port + 9000,
LocalPort: port + 9000,
}
// Some will succeed, some will fail due to validation
// The important thing is no race condition
_ = mutator.AddForward("dev", "default", fwd)
}(i)
}
// Wait for all goroutines
for i := 0; i < 10; i++ {
<-done
}
// Verify config is still valid
cfg, err := LoadConfig(configPath)
require.NoError(t, err)
require.NotNil(t, cfg)
}