Skip to content

Commit 2dbdffc

Browse files
authored
test(config): additional tests for drop-in configuration (#557)
* test(config): add drop-in configuration tests for toolset configs Add equivalent drop-in configuration tests for toolset_config_test.go that were added to provider_config_test.go in the recent commits: - TestConfigDirPathInContext - TestExtendedConfigMergingAcrossDropIns - TestExtendedConfigFromDropInOnly - TestStandaloneConfigDirWithExtendedConfig - TestConfigDirPathInContextStandalone Signed-off-by: Marc Nuri <[email protected]> * test(cmd): add --config-dir drop-in configuration tests Add CmdSuite with TestConfigDir to test the --config-dir flag behavior: - Standalone config-dir loading - Error when config-dir path is a file - Graceful skip for nonexistent directories - Merging with main config via --config flag - Lexical ordering of multiple drop-in files - CLI flags taking precedence over config-dir values Signed-off-by: Marc Nuri <[email protected]> --------- Signed-off-by: Marc Nuri <[email protected]>
1 parent c643889 commit 2dbdffc

File tree

2 files changed

+318
-0
lines changed

2 files changed

+318
-0
lines changed

pkg/config/toolset_config_test.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package config
33
import (
44
"context"
55
"errors"
6+
"os"
7+
"path/filepath"
68
"testing"
79

810
"github.com/BurntSushi/toml"
@@ -123,6 +125,210 @@ func (s *ToolsetConfigSuite) TestReadConfigUnregisteredToolsetConfig() {
123125
})
124126
}
125127

128+
func (s *ToolsetConfigSuite) TestConfigDirPathInContext() {
129+
var capturedDirPath string
130+
RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
131+
capturedDirPath = ConfigDirPathFromContext(ctx)
132+
var toolsetConfigForTest ToolsetConfigForTest
133+
if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil {
134+
return nil, err
135+
}
136+
return &toolsetConfigForTest, nil
137+
})
138+
configPath := s.writeConfig(`
139+
[toolset_configs.test-toolset]
140+
enabled = true
141+
endpoint = "https://example.com"
142+
timeout = 30
143+
`)
144+
145+
absConfigPath, err := filepath.Abs(configPath)
146+
s.Require().NoError(err, "test error: getting the absConfigPath should not fail")
147+
148+
_, err = Read(configPath, "")
149+
s.Run("provides config directory path in context to parser", func() {
150+
s.Require().NoError(err, "Expected no error reading config")
151+
s.NotEmpty(capturedDirPath, "Expected non-empty directory path in context")
152+
s.Equal(filepath.Dir(absConfigPath), capturedDirPath, "Expected directory path to match config file directory")
153+
})
154+
}
155+
156+
func (s *ToolsetConfigSuite) TestExtendedConfigMergingAcrossDropIns() {
157+
// Test that extended configs (toolset_configs) are properly merged
158+
// when scattered across multiple drop-in files
159+
RegisterToolsetConfig("test-toolset", toolsetConfigForTestParser)
160+
161+
tempDir := s.T().TempDir()
162+
163+
// Create main config with initial toolset config
164+
mainConfigPath := filepath.Join(tempDir, "config.toml")
165+
err := os.WriteFile(mainConfigPath, []byte(`
166+
[toolset_configs.test-toolset]
167+
enabled = false
168+
endpoint = "from-main"
169+
timeout = 1
170+
`), 0644)
171+
s.Require().NoError(err)
172+
173+
// Create drop-in directory
174+
dropInDir := filepath.Join(tempDir, "conf.d")
175+
s.Require().NoError(os.Mkdir(dropInDir, 0755))
176+
177+
// First drop-in overrides some fields
178+
err = os.WriteFile(filepath.Join(dropInDir, "10-override.toml"), []byte(`
179+
[toolset_configs.test-toolset]
180+
enabled = true
181+
timeout = 10
182+
`), 0644)
183+
s.Require().NoError(err)
184+
185+
// Second drop-in overrides other fields
186+
err = os.WriteFile(filepath.Join(dropInDir, "20-final.toml"), []byte(`
187+
[toolset_configs.test-toolset]
188+
endpoint = "from-drop-in"
189+
timeout = 42
190+
`), 0644)
191+
s.Require().NoError(err)
192+
193+
config, err := Read(mainConfigPath, "")
194+
s.Require().NoError(err)
195+
s.Require().NotNil(config)
196+
197+
toolsetConfig, ok := config.GetToolsetConfig("test-toolset")
198+
s.Require().True(ok, "Expected to find toolset config")
199+
200+
testConfig, ok := toolsetConfig.(*ToolsetConfigForTest)
201+
s.Require().True(ok, "Expected toolset config to be *ToolsetConfigForTest")
202+
203+
s.Run("merges enabled from first drop-in", func() {
204+
s.True(testConfig.Enabled, "enabled should be true from 10-override.toml")
205+
})
206+
207+
s.Run("merges endpoint from second drop-in", func() {
208+
s.Equal("from-drop-in", testConfig.Endpoint, "endpoint should be from 20-final.toml")
209+
})
210+
211+
s.Run("last drop-in wins for timeout", func() {
212+
s.Equal(42, testConfig.Timeout, "timeout should be 42 from 20-final.toml")
213+
})
214+
}
215+
216+
func (s *ToolsetConfigSuite) TestExtendedConfigFromDropInOnly() {
217+
// Test that extended configs work when defined only in drop-in files (not in main config)
218+
RegisterToolsetConfig("test-toolset", toolsetConfigForTestParser)
219+
220+
tempDir := s.T().TempDir()
221+
222+
// Create main config WITHOUT toolset config
223+
mainConfigPath := filepath.Join(tempDir, "config.toml")
224+
err := os.WriteFile(mainConfigPath, []byte(`
225+
log_level = 1
226+
`), 0644)
227+
s.Require().NoError(err)
228+
229+
// Create drop-in directory
230+
dropInDir := filepath.Join(tempDir, "conf.d")
231+
s.Require().NoError(os.Mkdir(dropInDir, 0755))
232+
233+
// Drop-in defines the toolset config
234+
err = os.WriteFile(filepath.Join(dropInDir, "10-toolset.toml"), []byte(`
235+
[toolset_configs.test-toolset]
236+
enabled = true
237+
endpoint = "from-drop-in-only"
238+
timeout = 99
239+
`), 0644)
240+
s.Require().NoError(err)
241+
242+
config, err := Read(mainConfigPath, "")
243+
s.Require().NoError(err)
244+
s.Require().NotNil(config)
245+
246+
toolsetConfig, ok := config.GetToolsetConfig("test-toolset")
247+
s.Require().True(ok, "Expected to find toolset config from drop-in")
248+
249+
testConfig, ok := toolsetConfig.(*ToolsetConfigForTest)
250+
s.Require().True(ok)
251+
252+
s.Run("loads extended config from drop-in only", func() {
253+
s.True(testConfig.Enabled)
254+
s.Equal("from-drop-in-only", testConfig.Endpoint)
255+
s.Equal(99, testConfig.Timeout)
256+
})
257+
}
258+
259+
func (s *ToolsetConfigSuite) TestStandaloneConfigDirWithExtendedConfig() {
260+
// Test that extended configs work with standalone --config-dir (no main config)
261+
RegisterToolsetConfig("test-toolset", toolsetConfigForTestParser)
262+
263+
tempDir := s.T().TempDir()
264+
265+
// Create drop-in files only (no main config)
266+
err := os.WriteFile(filepath.Join(tempDir, "10-base.toml"), []byte(`
267+
[toolset_configs.test-toolset]
268+
enabled = false
269+
endpoint = "base"
270+
timeout = 1
271+
`), 0644)
272+
s.Require().NoError(err)
273+
274+
err = os.WriteFile(filepath.Join(tempDir, "20-override.toml"), []byte(`
275+
[toolset_configs.test-toolset]
276+
enabled = true
277+
timeout = 100
278+
`), 0644)
279+
s.Require().NoError(err)
280+
281+
// Read with standalone config-dir (empty config path)
282+
config, err := Read("", tempDir)
283+
s.Require().NoError(err)
284+
s.Require().NotNil(config)
285+
286+
toolsetConfig, ok := config.GetToolsetConfig("test-toolset")
287+
s.Require().True(ok, "Expected to find toolset config in standalone mode")
288+
289+
testConfig, ok := toolsetConfig.(*ToolsetConfigForTest)
290+
s.Require().True(ok)
291+
292+
s.Run("merges extended config in standalone mode", func() {
293+
s.True(testConfig.Enabled, "enabled should be true from 20-override.toml")
294+
s.Equal("base", testConfig.Endpoint, "endpoint should be 'base' from 10-base.toml")
295+
s.Equal(100, testConfig.Timeout, "timeout should be 100 from 20-override.toml")
296+
})
297+
}
298+
299+
func (s *ToolsetConfigSuite) TestConfigDirPathInContextStandalone() {
300+
// Test that configDirPath is correctly set in context for standalone --config-dir
301+
var capturedDirPath string
302+
RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
303+
capturedDirPath = ConfigDirPathFromContext(ctx)
304+
var toolsetConfigForTest ToolsetConfigForTest
305+
if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil {
306+
return nil, err
307+
}
308+
return &toolsetConfigForTest, nil
309+
})
310+
311+
tempDir := s.T().TempDir()
312+
313+
err := os.WriteFile(filepath.Join(tempDir, "10-config.toml"), []byte(`
314+
[toolset_configs.test-toolset]
315+
enabled = true
316+
endpoint = "test"
317+
timeout = 1
318+
`), 0644)
319+
s.Require().NoError(err)
320+
321+
absTempDir, err := filepath.Abs(tempDir)
322+
s.Require().NoError(err)
323+
324+
_, err = Read("", tempDir)
325+
s.Run("provides config directory path in context for standalone mode", func() {
326+
s.Require().NoError(err)
327+
s.NotEmpty(capturedDirPath, "Expected non-empty directory path in context")
328+
s.Equal(absTempDir, capturedDirPath, "Expected directory path to match config-dir")
329+
})
330+
}
331+
126332
func TestToolsetConfig(t *testing.T) {
127333
suite.Run(t, new(ToolsetConfigSuite))
128334
}

pkg/kubernetes-mcp-server/cmd/root_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/stretchr/testify/assert"
1414
"github.com/stretchr/testify/require"
15+
"github.com/stretchr/testify/suite"
1516
"k8s.io/cli-runtime/pkg/genericiooptions"
1617
)
1718

@@ -131,6 +132,117 @@ func TestConfig(t *testing.T) {
131132
})
132133
}
133134

135+
type CmdSuite struct {
136+
suite.Suite
137+
testDataDir string
138+
}
139+
140+
func (s *CmdSuite) SetupSuite() {
141+
_, file, _, _ := runtime.Caller(0)
142+
s.testDataDir = filepath.Join(filepath.Dir(file), "testdata")
143+
}
144+
145+
func (s *CmdSuite) TestConfigDir() {
146+
s.Run("set with --config-dir standalone", func() {
147+
dropInDir := s.T().TempDir()
148+
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "10-config.toml"), []byte(`
149+
list_output = "yaml"
150+
read_only = true
151+
disable_destructive = true
152+
`), 0644))
153+
154+
ioStreams, out := testStream()
155+
rootCmd := NewMCPServer(ioStreams)
156+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config-dir", dropInDir})
157+
s.Require().NoError(rootCmd.Execute())
158+
s.Contains(out.String(), "ListOutput: yaml")
159+
s.Contains(out.String(), "Read-only mode: true")
160+
s.Contains(out.String(), "Disable destructive tools: true")
161+
})
162+
s.Run("--config-dir path is a file throws error", func() {
163+
tempDir := s.T().TempDir()
164+
filePath := filepath.Join(tempDir, "not-a-directory.toml")
165+
s.Require().NoError(os.WriteFile(filePath, []byte("log_level = 1"), 0644))
166+
167+
ioStreams, _ := testStream()
168+
rootCmd := NewMCPServer(ioStreams)
169+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config-dir", filePath})
170+
err := rootCmd.Execute()
171+
s.Require().Error(err)
172+
s.Contains(err.Error(), "drop-in config path is not a directory")
173+
})
174+
s.Run("nonexistent --config-dir is silently skipped", func() {
175+
ioStreams, out := testStream()
176+
rootCmd := NewMCPServer(ioStreams)
177+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config-dir", "/nonexistent/path/to/config-dir"})
178+
err := rootCmd.Execute()
179+
s.Require().NoError(err, "Nonexistent directories should be gracefully skipped")
180+
s.Contains(out.String(), "ListOutput: table", "Default values should be used")
181+
})
182+
s.Run("--config with --config-dir merges configs", func() {
183+
tempDir := s.T().TempDir()
184+
mainConfigPath := filepath.Join(tempDir, "config.toml")
185+
s.Require().NoError(os.WriteFile(mainConfigPath, []byte(`
186+
list_output = "table"
187+
read_only = false
188+
`), 0644))
189+
190+
dropInDir := filepath.Join(tempDir, "conf.d")
191+
s.Require().NoError(os.Mkdir(dropInDir, 0755))
192+
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "10-override.toml"), []byte(`
193+
read_only = true
194+
disable_destructive = true
195+
`), 0644))
196+
197+
ioStreams, out := testStream()
198+
rootCmd := NewMCPServer(ioStreams)
199+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config", mainConfigPath, "--config-dir", dropInDir})
200+
s.Require().NoError(rootCmd.Execute())
201+
s.Contains(out.String(), "ListOutput: table", "list_output from main config")
202+
s.Contains(out.String(), "Read-only mode: true", "read_only overridden by drop-in")
203+
s.Contains(out.String(), "Disable destructive tools: true", "disable_destructive from drop-in")
204+
})
205+
s.Run("multiple drop-in files are merged in order", func() {
206+
dropInDir := s.T().TempDir()
207+
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "10-first.toml"), []byte(`
208+
list_output = "yaml"
209+
read_only = true
210+
`), 0644))
211+
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "20-second.toml"), []byte(`
212+
list_output = "table"
213+
disable_destructive = true
214+
`), 0644))
215+
216+
ioStreams, out := testStream()
217+
rootCmd := NewMCPServer(ioStreams)
218+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--config-dir", dropInDir})
219+
s.Require().NoError(rootCmd.Execute())
220+
s.Contains(out.String(), "ListOutput: table", "list_output from 20-second.toml (last wins)")
221+
s.Contains(out.String(), "Read-only mode: true", "read_only from 10-first.toml")
222+
s.Contains(out.String(), "Disable destructive tools: true", "disable_destructive from 20-second.toml")
223+
})
224+
s.Run("flags take precedence over --config-dir", func() {
225+
dropInDir := s.T().TempDir()
226+
s.Require().NoError(os.WriteFile(filepath.Join(dropInDir, "10-config.toml"), []byte(`
227+
list_output = "yaml"
228+
read_only = true
229+
disable_destructive = true
230+
`), 0644))
231+
232+
ioStreams, out := testStream()
233+
rootCmd := NewMCPServer(ioStreams)
234+
rootCmd.SetArgs([]string{"--version", "--port=1337", "--log-level=1", "--list-output=table", "--read-only=false", "--disable-destructive=false", "--config-dir", dropInDir})
235+
s.Require().NoError(rootCmd.Execute())
236+
s.Contains(out.String(), "ListOutput: table", "flag takes precedence")
237+
s.Contains(out.String(), "Read-only mode: false", "flag takes precedence")
238+
s.Contains(out.String(), "Disable destructive tools: false", "flag takes precedence")
239+
})
240+
}
241+
242+
func TestCmd(t *testing.T) {
243+
suite.Run(t, new(CmdSuite))
244+
}
245+
134246
func TestToolsets(t *testing.T) {
135247
t.Run("available", func(t *testing.T) {
136248
ioStreams, _ := testStream()

0 commit comments

Comments
 (0)