Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions pkg/config/toolset_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package config
import (
"context"
"errors"
"os"
"path/filepath"
"testing"

"github.com/BurntSushi/toml"
Expand Down Expand Up @@ -123,6 +125,210 @@ func (s *ToolsetConfigSuite) TestReadConfigUnregisteredToolsetConfig() {
})
}

func (s *ToolsetConfigSuite) TestConfigDirPathInContext() {
var capturedDirPath string
RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
capturedDirPath = ConfigDirPathFromContext(ctx)
var toolsetConfigForTest ToolsetConfigForTest
if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil {
return nil, err
}
return &toolsetConfigForTest, nil
})
configPath := s.writeConfig(`
[toolset_configs.test-toolset]
enabled = true
endpoint = "https://example.com"
timeout = 30
`)

absConfigPath, err := filepath.Abs(configPath)
s.Require().NoError(err, "test error: getting the absConfigPath should not fail")

_, err = Read(configPath, "")
s.Run("provides config directory path in context to parser", func() {
s.Require().NoError(err, "Expected no error reading config")
s.NotEmpty(capturedDirPath, "Expected non-empty directory path in context")
s.Equal(filepath.Dir(absConfigPath), capturedDirPath, "Expected directory path to match config file directory")
})
}

func (s *ToolsetConfigSuite) TestExtendedConfigMergingAcrossDropIns() {
// Test that extended configs (toolset_configs) are properly merged
// when scattered across multiple drop-in files
RegisterToolsetConfig("test-toolset", toolsetConfigForTestParser)

tempDir := s.T().TempDir()

// Create main config with initial toolset config
mainConfigPath := filepath.Join(tempDir, "config.toml")
err := os.WriteFile(mainConfigPath, []byte(`
[toolset_configs.test-toolset]
enabled = false
endpoint = "from-main"
timeout = 1
`), 0644)
s.Require().NoError(err)

// Create drop-in directory
dropInDir := filepath.Join(tempDir, "conf.d")
s.Require().NoError(os.Mkdir(dropInDir, 0755))

// First drop-in overrides some fields
err = os.WriteFile(filepath.Join(dropInDir, "10-override.toml"), []byte(`
[toolset_configs.test-toolset]
enabled = true
timeout = 10
`), 0644)
s.Require().NoError(err)

// Second drop-in overrides other fields
err = os.WriteFile(filepath.Join(dropInDir, "20-final.toml"), []byte(`
[toolset_configs.test-toolset]
endpoint = "from-drop-in"
timeout = 42
`), 0644)
s.Require().NoError(err)

config, err := Read(mainConfigPath, "")
s.Require().NoError(err)
s.Require().NotNil(config)

toolsetConfig, ok := config.GetToolsetConfig("test-toolset")
s.Require().True(ok, "Expected to find toolset config")

testConfig, ok := toolsetConfig.(*ToolsetConfigForTest)
s.Require().True(ok, "Expected toolset config to be *ToolsetConfigForTest")

s.Run("merges enabled from first drop-in", func() {
s.True(testConfig.Enabled, "enabled should be true from 10-override.toml")
})

s.Run("merges endpoint from second drop-in", func() {
s.Equal("from-drop-in", testConfig.Endpoint, "endpoint should be from 20-final.toml")
})

s.Run("last drop-in wins for timeout", func() {
s.Equal(42, testConfig.Timeout, "timeout should be 42 from 20-final.toml")
})
}

func (s *ToolsetConfigSuite) TestExtendedConfigFromDropInOnly() {
// Test that extended configs work when defined only in drop-in files (not in main config)
RegisterToolsetConfig("test-toolset", toolsetConfigForTestParser)

tempDir := s.T().TempDir()

// Create main config WITHOUT toolset config
mainConfigPath := filepath.Join(tempDir, "config.toml")
err := os.WriteFile(mainConfigPath, []byte(`
log_level = 1
`), 0644)
s.Require().NoError(err)

// Create drop-in directory
dropInDir := filepath.Join(tempDir, "conf.d")
s.Require().NoError(os.Mkdir(dropInDir, 0755))

// Drop-in defines the toolset config
err = os.WriteFile(filepath.Join(dropInDir, "10-toolset.toml"), []byte(`
[toolset_configs.test-toolset]
enabled = true
endpoint = "from-drop-in-only"
timeout = 99
`), 0644)
s.Require().NoError(err)

config, err := Read(mainConfigPath, "")
s.Require().NoError(err)
s.Require().NotNil(config)

toolsetConfig, ok := config.GetToolsetConfig("test-toolset")
s.Require().True(ok, "Expected to find toolset config from drop-in")

testConfig, ok := toolsetConfig.(*ToolsetConfigForTest)
s.Require().True(ok)

s.Run("loads extended config from drop-in only", func() {
s.True(testConfig.Enabled)
s.Equal("from-drop-in-only", testConfig.Endpoint)
s.Equal(99, testConfig.Timeout)
})
}

func (s *ToolsetConfigSuite) TestStandaloneConfigDirWithExtendedConfig() {
// Test that extended configs work with standalone --config-dir (no main config)
RegisterToolsetConfig("test-toolset", toolsetConfigForTestParser)

tempDir := s.T().TempDir()

// Create drop-in files only (no main config)
err := os.WriteFile(filepath.Join(tempDir, "10-base.toml"), []byte(`
[toolset_configs.test-toolset]
enabled = false
endpoint = "base"
timeout = 1
`), 0644)
s.Require().NoError(err)

err = os.WriteFile(filepath.Join(tempDir, "20-override.toml"), []byte(`
[toolset_configs.test-toolset]
enabled = true
timeout = 100
`), 0644)
s.Require().NoError(err)

// Read with standalone config-dir (empty config path)
config, err := Read("", tempDir)
s.Require().NoError(err)
s.Require().NotNil(config)

toolsetConfig, ok := config.GetToolsetConfig("test-toolset")
s.Require().True(ok, "Expected to find toolset config in standalone mode")

testConfig, ok := toolsetConfig.(*ToolsetConfigForTest)
s.Require().True(ok)

s.Run("merges extended config in standalone mode", func() {
s.True(testConfig.Enabled, "enabled should be true from 20-override.toml")
s.Equal("base", testConfig.Endpoint, "endpoint should be 'base' from 10-base.toml")
s.Equal(100, testConfig.Timeout, "timeout should be 100 from 20-override.toml")
})
}

func (s *ToolsetConfigSuite) TestConfigDirPathInContextStandalone() {
// Test that configDirPath is correctly set in context for standalone --config-dir
var capturedDirPath string
RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
capturedDirPath = ConfigDirPathFromContext(ctx)
var toolsetConfigForTest ToolsetConfigForTest
if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil {
return nil, err
}
return &toolsetConfigForTest, nil
})

tempDir := s.T().TempDir()

err := os.WriteFile(filepath.Join(tempDir, "10-config.toml"), []byte(`
[toolset_configs.test-toolset]
enabled = true
endpoint = "test"
timeout = 1
`), 0644)
s.Require().NoError(err)

absTempDir, err := filepath.Abs(tempDir)
s.Require().NoError(err)

_, err = Read("", tempDir)
s.Run("provides config directory path in context for standalone mode", func() {
s.Require().NoError(err)
s.NotEmpty(capturedDirPath, "Expected non-empty directory path in context")
s.Equal(absTempDir, capturedDirPath, "Expected directory path to match config-dir")
})
}

func TestToolsetConfig(t *testing.T) {
suite.Run(t, new(ToolsetConfigSuite))
}
112 changes: 112 additions & 0 deletions pkg/kubernetes-mcp-server/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"k8s.io/cli-runtime/pkg/genericiooptions"
)

Expand Down Expand Up @@ -131,6 +132,117 @@ func TestConfig(t *testing.T) {
})
}

type CmdSuite struct {
suite.Suite
testDataDir string
}

func (s *CmdSuite) SetupSuite() {
_, file, _, _ := runtime.Caller(0)
s.testDataDir = filepath.Join(filepath.Dir(file), "testdata")
}

func (s *CmdSuite) TestConfigDir() {
s.Run("set with --config-dir standalone", func() {
dropInDir := s.T().TempDir()
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "10-config.toml"), []byte(`
list_output = "yaml"
read_only = true
disable_destructive = true
`), 0644))

ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config-dir", dropInDir})
s.Require().NoError(rootCmd.Execute())
s.Contains(out.String(), "ListOutput: yaml")
s.Contains(out.String(), "Read-only mode: true")
s.Contains(out.String(), "Disable destructive tools: true")
})
s.Run("--config-dir path is a file throws error", func() {
tempDir := s.T().TempDir()
filePath := filepath.Join(tempDir, "not-a-directory.toml")
s.Require().NoError(os.WriteFile(filePath, []byte("log_level = 1"), 0644))

ioStreams, _ := testStream()
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config-dir", filePath})
err := rootCmd.Execute()
s.Require().Error(err)
s.Contains(err.Error(), "drop-in config path is not a directory")
})
s.Run("nonexistent --config-dir is silently skipped", func() {
ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config-dir", "/nonexistent/path/to/config-dir"})
err := rootCmd.Execute()
s.Require().NoError(err, "Nonexistent directories should be gracefully skipped")
s.Contains(out.String(), "ListOutput: table", "Default values should be used")
})
s.Run("--config with --config-dir merges configs", func() {
tempDir := s.T().TempDir()
mainConfigPath := filepath.Join(tempDir, "config.toml")
s.Require().NoError(os.WriteFile(mainConfigPath, []byte(`
list_output = "table"
read_only = false
`), 0644))

dropInDir := filepath.Join(tempDir, "conf.d")
s.Require().NoError(os.Mkdir(dropInDir, 0755))
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "10-override.toml"), []byte(`
read_only = true
disable_destructive = true
`), 0644))

ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config", mainConfigPath, "--config-dir", dropInDir})
s.Require().NoError(rootCmd.Execute())
s.Contains(out.String(), "ListOutput: table", "list_output from main config")
s.Contains(out.String(), "Read-only mode: true", "read_only overridden by drop-in")
s.Contains(out.String(), "Disable destructive tools: true", "disable_destructive from drop-in")
})
s.Run("multiple drop-in files are merged in order", func() {
dropInDir := s.T().TempDir()
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "10-first.toml"), []byte(`
list_output = "yaml"
read_only = true
`), 0644))
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "20-second.toml"), []byte(`
list_output = "table"
disable_destructive = true
`), 0644))

ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config-dir", dropInDir})
s.Require().NoError(rootCmd.Execute())
s.Contains(out.String(), "ListOutput: table", "list_output from 20-second.toml (last wins)")
s.Contains(out.String(), "Read-only mode: true", "read_only from 10-first.toml")
s.Contains(out.String(), "Disable destructive tools: true", "disable_destructive from 20-second.toml")
})
s.Run("flags take precedence over --config-dir", func() {
dropInDir := s.T().TempDir()
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "10-config.toml"), []byte(`
list_output = "yaml"
read_only = true
disable_destructive = true
`), 0644))

ioStreams, out := testStream()
rootCmd := NewMCPServer(ioStreams)
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--list-output=table", "--read-only=false", "--disable-destructive=false", "--config-dir", dropInDir})
s.Require().NoError(rootCmd.Execute())
s.Contains(out.String(), "ListOutput: table", "flag takes precedence")
s.Contains(out.String(), "Read-only mode: false", "flag takes precedence")
s.Contains(out.String(), "Disable destructive tools: false", "flag takes precedence")
})
}

func TestCmd(t *testing.T) {
suite.Run(t, new(CmdSuite))
}

func TestToolsets(t *testing.T) {
t.Run("available", func(t *testing.T) {
ioStreams, _ := testStream()
Expand Down