|
8 | 8 | "github.com/stretchr/testify/assert"
|
9 | 9 | "github.com/stretchr/testify/require"
|
10 | 10 |
|
| 11 | + "github.com/stacklok/toolhive/pkg/environment" |
11 | 12 | "github.com/stacklok/toolhive/pkg/runner"
|
| 13 | + "github.com/stacklok/toolhive/pkg/transport/types" |
12 | 14 | )
|
13 | 15 |
|
14 | 16 | func TestRunCmdFlagParsing(t *testing.T) {
|
@@ -353,3 +355,137 @@ func TestTryLoadConfigFromFile(t *testing.T) {
|
353 | 355 | assert.Contains(t, err.Error(), "failed to parse JSON")
|
354 | 356 | })
|
355 | 357 | }
|
| 358 | + |
| 359 | +func TestRunCmdEnvironmentVariableOverrideBehavior(t *testing.T) { |
| 360 | + t.Parallel() |
| 361 | + |
| 362 | + t.Run("no CLI env vars preserves file-based env vars", func(t *testing.T) { |
| 363 | + t.Parallel() |
| 364 | + |
| 365 | + // Create a config file with environment variables |
| 366 | + tmpDir := t.TempDir() |
| 367 | + configPath := tmpDir + "/runconfig.json" |
| 368 | + |
| 369 | + configContent := `{ |
| 370 | + "schema_version": "v1", |
| 371 | + "name": "env-test-server", |
| 372 | + "image": "test:env", |
| 373 | + "transport": "stdio", |
| 374 | + "env_vars": { |
| 375 | + "FILE_VAR": "file_value", |
| 376 | + "COMMON_VAR": "from_file" |
| 377 | + } |
| 378 | + }` |
| 379 | + |
| 380 | + err := os.WriteFile(configPath, []byte(configContent), 0644) |
| 381 | + require.NoError(t, err, "Should be able to create config file") |
| 382 | + |
| 383 | + // Test function that loads config from our specific path |
| 384 | + testLoadConfigFromPath := func(path string) (*runner.RunConfig, error) { |
| 385 | + if _, err := os.Stat(path); err != nil { |
| 386 | + return nil, nil |
| 387 | + } |
| 388 | + |
| 389 | + file, err := os.Open(path) // #nosec G304 - test path |
| 390 | + if err != nil { |
| 391 | + return nil, fmt.Errorf("found config file at %s but failed to open: %w", path, err) |
| 392 | + } |
| 393 | + defer file.Close() |
| 394 | + |
| 395 | + return runner.ReadJSON(file) |
| 396 | + } |
| 397 | + |
| 398 | + config, err := testLoadConfigFromPath(configPath) |
| 399 | + require.NoError(t, err, "Should successfully load config from file") |
| 400 | + require.NotNil(t, config, "Should return config when file exists") |
| 401 | + |
| 402 | + // Verify file-based environment variables are present |
| 403 | + assert.Equal(t, "file_value", config.EnvVars["FILE_VAR"]) |
| 404 | + assert.Equal(t, "from_file", config.EnvVars["COMMON_VAR"]) |
| 405 | + |
| 406 | + // Create a mock command with no --env flags set |
| 407 | + cmd := NewRunCmd() |
| 408 | + require.NotNil(t, cmd) |
| 409 | + addRunFlags(cmd, &proxyRunFlags{}) |
| 410 | + |
| 411 | + // Parse empty args (no --env flags) |
| 412 | + err = cmd.ParseFlags([]string{"--transport", "stdio", "--name", "test-server"}) |
| 413 | + require.NoError(t, err) |
| 414 | + |
| 415 | + // Verify that env flags were not changed |
| 416 | + assert.False(t, cmd.Flags().Changed("env"), "env flag should not be changed") |
| 417 | + }) |
| 418 | + |
| 419 | + t.Run("CLI env vars override file-based env vars", func(t *testing.T) { |
| 420 | + t.Parallel() |
| 421 | + |
| 422 | + // Create a config file with environment variables |
| 423 | + tmpDir := t.TempDir() |
| 424 | + configPath := tmpDir + "/runconfig.json" |
| 425 | + |
| 426 | + configContent := `{ |
| 427 | + "schema_version": "v1", |
| 428 | + "name": "env-test-server", |
| 429 | + "image": "test:env", |
| 430 | + "transport": "stdio", |
| 431 | + "env_vars": { |
| 432 | + "FILE_VAR": "file_value", |
| 433 | + "COMMON_VAR": "from_file" |
| 434 | + } |
| 435 | + }` |
| 436 | + |
| 437 | + err := os.WriteFile(configPath, []byte(configContent), 0644) |
| 438 | + require.NoError(t, err, "Should be able to create config file") |
| 439 | + |
| 440 | + // Create a mock command with --env flags set |
| 441 | + cmd := NewRunCmd() |
| 442 | + require.NotNil(t, cmd) |
| 443 | + flags := &proxyRunFlags{} |
| 444 | + addRunFlags(cmd, flags) |
| 445 | + |
| 446 | + // Parse args with --env flags that should override file-based env vars |
| 447 | + err = cmd.ParseFlags([]string{ |
| 448 | + "--transport", "stdio", |
| 449 | + "--name", "test-server", |
| 450 | + "--env", "CLI_VAR=cli_value", |
| 451 | + "--env", "COMMON_VAR=from_cli", |
| 452 | + }) |
| 453 | + require.NoError(t, err) |
| 454 | + |
| 455 | + // Verify that env flag was changed and values are correct |
| 456 | + assert.True(t, cmd.Flags().Changed("env"), "env flag should be changed") |
| 457 | + assert.Contains(t, flags.runEnv, "CLI_VAR=cli_value") |
| 458 | + assert.Contains(t, flags.runEnv, "COMMON_VAR=from_cli") |
| 459 | + }) |
| 460 | + |
| 461 | + t.Run("empty CLI env vars do not interfere with file-based env vars", func(t *testing.T) { |
| 462 | + t.Parallel() |
| 463 | + |
| 464 | + // Test that ParseEnvironmentVariables with empty slice returns empty map |
| 465 | + envVarsMap, err := environment.ParseEnvironmentVariables([]string{}) |
| 466 | + require.NoError(t, err, "ParseEnvironmentVariables should handle empty slice") |
| 467 | + assert.Equal(t, map[string]string{}, envVarsMap, "Empty slice should return empty map") |
| 468 | + assert.Len(t, envVarsMap, 0, "Empty map should have zero length") |
| 469 | + |
| 470 | + // Create a RunConfig with some existing env vars (simulating file-based config) |
| 471 | + config := &runner.RunConfig{ |
| 472 | + Transport: types.TransportTypeStdio, |
| 473 | + TargetPort: 8080, |
| 474 | + EnvVars: map[string]string{ |
| 475 | + "FILE_VAR": "file_value", |
| 476 | + "COMMON_VAR": "from_file", |
| 477 | + }, |
| 478 | + } |
| 479 | + |
| 480 | + // Apply empty environment variables (simulating no CLI --env flags) |
| 481 | + resultConfig, err := config.WithEnvironmentVariables(envVarsMap) |
| 482 | + require.NoError(t, err, "WithEnvironmentVariables should handle empty map") |
| 483 | + |
| 484 | + // Verify that original env vars are preserved |
| 485 | + assert.Equal(t, "file_value", resultConfig.EnvVars["FILE_VAR"], "File-based env var should be preserved") |
| 486 | + assert.Equal(t, "from_file", resultConfig.EnvVars["COMMON_VAR"], "File-based env var should be preserved") |
| 487 | + |
| 488 | + // Verify transport-specific env vars are also set (these are always added) |
| 489 | + assert.Equal(t, "stdio", resultConfig.EnvVars["MCP_TRANSPORT"], "Transport env var should be set") |
| 490 | + }) |
| 491 | +} |
0 commit comments