diff --git a/internal/config/validation_env_test.go b/internal/config/validation_env_test.go index 045e3ece..266e8fb5 100644 --- a/internal/config/validation_env_test.go +++ b/internal/config/validation_env_test.go @@ -539,3 +539,341 @@ func TestRunDockerInspect(t *testing.T) { }) } } + +func TestCheckDockerAccessible(t *testing.T) { + t.Run("check docker accessibility", func(t *testing.T) { + // This test verifies the function runs without panicking + // In CI environments, Docker may or may not be available + result := checkDockerAccessible() + t.Logf("Docker accessible: %v", result) + // We don't assert the result since Docker availability varies by environment + }) + + t.Run("with custom DOCKER_HOST", func(t *testing.T) { + // Test with a custom DOCKER_HOST that doesn't exist + originalHost := os.Getenv("DOCKER_HOST") + defer func() { + if originalHost != "" { + os.Setenv("DOCKER_HOST", originalHost) + } else { + os.Unsetenv("DOCKER_HOST") + } + }() + + os.Setenv("DOCKER_HOST", "unix:///nonexistent/docker.sock") + result := checkDockerAccessible() + assert.False(t, result, "Should return false for nonexistent socket") + }) + + t.Run("with unix:// prefix in DOCKER_HOST", func(t *testing.T) { + originalHost := os.Getenv("DOCKER_HOST") + defer func() { + if originalHost != "" { + os.Setenv("DOCKER_HOST", originalHost) + } else { + os.Unsetenv("DOCKER_HOST") + } + }() + + // Set DOCKER_HOST with unix:// prefix + os.Setenv("DOCKER_HOST", "unix:///var/run/docker.sock") + // Function should strip the unix:// prefix and check the path + checkDockerAccessible() + // If it doesn't panic, the prefix stripping works + }) +} + +func TestCheckPortMapping(t *testing.T) { + tests := []struct { + name string + containerID string + port string + shouldError bool + }{ + { + name: "empty container ID", + containerID: "", + port: "8080", + shouldError: true, + }, + { + name: "invalid container ID", + containerID: "invalid;id", + port: "8080", + shouldError: true, + }, + { + name: "valid container ID format - nonexistent container", + containerID: "abcdef123456", + port: "8080", + shouldError: true, // Will fail because container doesn't exist + }, + { + name: "empty port", + containerID: "abcdef123456", + port: "", + shouldError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mapped, err := checkPortMapping(tt.containerID, tt.port) + + if tt.shouldError { + assert.Error(t, err, "Expected error for %s", tt.name) + assert.False(t, mapped, "Port should not be mapped on error") + } else { + assert.NoError(t, err, "Unexpected error") + } + }) + } +} + +func TestCheckStdinInteractive(t *testing.T) { + tests := []struct { + name string + containerID string + expected bool + }{ + { + name: "empty container ID", + containerID: "", + expected: false, + }, + { + name: "invalid container ID", + containerID: "invalid;id", + expected: false, + }, + { + name: "valid container ID format - nonexistent container", + containerID: "abcdef123456", + expected: false, // Will fail because container doesn't exist + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := checkStdinInteractive(tt.containerID) + assert.Equal(t, tt.expected, result, "Unexpected result for %s", tt.name) + }) + } +} + +func TestCheckLogDirMounted(t *testing.T) { + tests := []struct { + name string + containerID string + logDir string + expected bool + }{ + { + name: "empty container ID", + containerID: "", + logDir: "/tmp/gh-aw/mcp-logs", + expected: false, + }, + { + name: "invalid container ID", + containerID: "invalid;id", + logDir: "/tmp/gh-aw/mcp-logs", + expected: false, + }, + { + name: "valid container ID format - nonexistent container", + containerID: "abcdef123456", + logDir: "/tmp/gh-aw/mcp-logs", + expected: false, // Will fail because container doesn't exist + }, + { + name: "empty log directory", + containerID: "abcdef123456", + logDir: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := checkLogDirMounted(tt.containerID, tt.logDir) + assert.Equal(t, tt.expected, result, "Unexpected result for %s", tt.name) + }) + } +} + +func TestValidateContainerizedEnvironment(t *testing.T) { + // Save original env vars + origPort := os.Getenv("MCP_GATEWAY_PORT") + origDomain := os.Getenv("MCP_GATEWAY_DOMAIN") + origAPIKey := os.Getenv("MCP_GATEWAY_API_KEY") + origLogDir := os.Getenv("MCP_GATEWAY_LOG_DIR") + defer func() { + if origPort != "" { + os.Setenv("MCP_GATEWAY_PORT", origPort) + } else { + os.Unsetenv("MCP_GATEWAY_PORT") + } + if origDomain != "" { + os.Setenv("MCP_GATEWAY_DOMAIN", origDomain) + } else { + os.Unsetenv("MCP_GATEWAY_DOMAIN") + } + if origAPIKey != "" { + os.Setenv("MCP_GATEWAY_API_KEY", origAPIKey) + } else { + os.Unsetenv("MCP_GATEWAY_API_KEY") + } + if origLogDir != "" { + os.Setenv("MCP_GATEWAY_LOG_DIR", origLogDir) + } else { + os.Unsetenv("MCP_GATEWAY_LOG_DIR") + } + }() + + t.Run("empty container ID", func(t *testing.T) { + os.Setenv("MCP_GATEWAY_PORT", "8080") + os.Setenv("MCP_GATEWAY_DOMAIN", "localhost") + os.Setenv("MCP_GATEWAY_API_KEY", "test-key") + + result := ValidateContainerizedEnvironment("") + + assert.True(t, result.IsContainerized, "Should be marked as containerized") + assert.Equal(t, "", result.ContainerID, "Container ID should be empty") + assert.False(t, result.IsValid(), "Should be invalid with empty container ID") + assert.Contains(t, result.Error(), "Container ID could not be determined") + }) + + t.Run("valid container ID with all env vars", func(t *testing.T) { + os.Setenv("MCP_GATEWAY_PORT", "8080") + os.Setenv("MCP_GATEWAY_DOMAIN", "localhost") + os.Setenv("MCP_GATEWAY_API_KEY", "test-key") + + result := ValidateContainerizedEnvironment("abcdef123456") + + assert.True(t, result.IsContainerized, "Should be marked as containerized") + assert.Equal(t, "abcdef123456", result.ContainerID) + // Will fail validation because Docker checks will fail in test environment + // but we verify the container ID was set correctly + }) + + t.Run("missing required env vars", func(t *testing.T) { + os.Unsetenv("MCP_GATEWAY_PORT") + os.Unsetenv("MCP_GATEWAY_DOMAIN") + os.Unsetenv("MCP_GATEWAY_API_KEY") + + result := ValidateContainerizedEnvironment("abcdef123456") + + assert.True(t, result.IsContainerized) + assert.Equal(t, "abcdef123456", result.ContainerID) + assert.False(t, result.IsValid(), "Should be invalid with missing env vars") + assert.Len(t, result.MissingEnvVars, 3, "Should have 3 missing env vars") + }) + + t.Run("port validation failure", func(t *testing.T) { + os.Setenv("MCP_GATEWAY_PORT", "8080") + os.Setenv("MCP_GATEWAY_DOMAIN", "localhost") + os.Setenv("MCP_GATEWAY_API_KEY", "test-key") + + result := ValidateContainerizedEnvironment("abcdef123456") + + assert.True(t, result.IsContainerized) + // Port mapping check will fail (container doesn't exist) + assert.False(t, result.PortMapped, "Port should not be mapped for nonexistent container") + }) + + t.Run("stdin interactive check", func(t *testing.T) { + os.Setenv("MCP_GATEWAY_PORT", "8080") + os.Setenv("MCP_GATEWAY_DOMAIN", "localhost") + os.Setenv("MCP_GATEWAY_API_KEY", "test-key") + + result := ValidateContainerizedEnvironment("abcdef123456") + + assert.True(t, result.IsContainerized) + // Stdin check will fail (container doesn't exist) + assert.False(t, result.StdinInteractive, "Stdin should not be interactive for nonexistent container") + }) + + t.Run("log directory mount check with default", func(t *testing.T) { + os.Setenv("MCP_GATEWAY_PORT", "8080") + os.Setenv("MCP_GATEWAY_DOMAIN", "localhost") + os.Setenv("MCP_GATEWAY_API_KEY", "test-key") + os.Unsetenv("MCP_GATEWAY_LOG_DIR") + + result := ValidateContainerizedEnvironment("abcdef123456") + + assert.True(t, result.IsContainerized) + // Log dir check will fail (container doesn't exist) + assert.False(t, result.LogDirMounted, "Log dir should not be mounted for nonexistent container") + // Should have a warning about log dir not being mounted + assert.Greater(t, len(result.ValidationWarnings), 0, "Should have warnings") + }) + + t.Run("log directory mount check with custom dir", func(t *testing.T) { + os.Setenv("MCP_GATEWAY_PORT", "8080") + os.Setenv("MCP_GATEWAY_DOMAIN", "localhost") + os.Setenv("MCP_GATEWAY_API_KEY", "test-key") + os.Setenv("MCP_GATEWAY_LOG_DIR", "/custom/log/path") + + result := ValidateContainerizedEnvironment("abcdef123456") + + assert.True(t, result.IsContainerized) + assert.False(t, result.LogDirMounted) + // Verify the warning mentions the custom path + hasCustomPathWarning := false + for _, warning := range result.ValidationWarnings { + if assert.Contains(t, warning, "/custom/log/path") { + hasCustomPathWarning = true + break + } + } + if len(result.ValidationWarnings) > 0 { + assert.True(t, hasCustomPathWarning, "Should have warning with custom log path") + } + }) + + t.Run("docker not accessible", func(t *testing.T) { + // Set a DOCKER_HOST that doesn't exist + originalHost := os.Getenv("DOCKER_HOST") + defer func() { + if originalHost != "" { + os.Setenv("DOCKER_HOST", originalHost) + } else { + os.Unsetenv("DOCKER_HOST") + } + }() + + os.Setenv("DOCKER_HOST", "unix:///nonexistent/docker.sock") + os.Setenv("MCP_GATEWAY_PORT", "8080") + os.Setenv("MCP_GATEWAY_DOMAIN", "localhost") + os.Setenv("MCP_GATEWAY_API_KEY", "test-key") + + result := ValidateContainerizedEnvironment("abcdef123456") + + assert.False(t, result.DockerAccessible, "Docker should not be accessible") + assert.False(t, result.IsValid(), "Should be invalid when Docker is not accessible") + // Should have error about Docker not being accessible + hasDockerError := false + for _, err := range result.ValidationErrors { + if assert.Contains(t, err, "Docker daemon") { + hasDockerError = true + break + } + } + assert.True(t, hasDockerError, "Should have Docker accessibility error") + }) + + t.Run("validation result error message format", func(t *testing.T) { + os.Unsetenv("MCP_GATEWAY_PORT") + os.Unsetenv("MCP_GATEWAY_DOMAIN") + os.Unsetenv("MCP_GATEWAY_API_KEY") + + result := ValidateContainerizedEnvironment("abcdef123456") + + errorMsg := result.Error() + assert.NotEmpty(t, errorMsg, "Error message should not be empty") + assert.Contains(t, errorMsg, "Environment validation failed", "Error should have header") + // Each error should be on its own line with bullet point + assert.Contains(t, errorMsg, "\n - ", "Errors should be formatted with bullets") + }) +}