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), 0644) 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), 0644) 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 forward Forward expectedID string }{ { 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 forward Forward expectedString string }{ { 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 { name string config *Config 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"), 0644) 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)) }