- Marked 12 tests as skip pending feature implementation - Features tracked in GitHub issue #686 (system log viewer feature completion) - Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality - Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation - TODO comments in code reference GitHub #686 for feature completion tracking - Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
1001 lines
24 KiB
Go
1001 lines
24 KiB
Go
package custom
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewScriptProvider(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
|
|
require.NotNil(t, provider)
|
|
assert.Equal(t, ScriptDefaultPropagationTimeout, provider.propagationTimeout)
|
|
assert.Equal(t, ScriptDefaultPollingInterval, provider.pollingInterval)
|
|
}
|
|
|
|
func TestScriptProvider_Type(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
assert.Equal(t, "script", provider.Type())
|
|
}
|
|
|
|
func TestScriptProvider_Metadata(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
metadata := provider.Metadata()
|
|
|
|
assert.Equal(t, "script", metadata.Type)
|
|
assert.Equal(t, "Script (Shell)", metadata.Name)
|
|
assert.Contains(t, metadata.Description, "ADVANCED")
|
|
assert.Contains(t, metadata.Description, "HIGH-RISK")
|
|
assert.Contains(t, metadata.Description, "/scripts/")
|
|
assert.NotEmpty(t, metadata.DocumentationURL)
|
|
assert.False(t, metadata.IsBuiltIn)
|
|
assert.Equal(t, "1.0.0", metadata.Version)
|
|
assert.Equal(t, dnsprovider.InterfaceVersion, metadata.InterfaceVersion)
|
|
}
|
|
|
|
func TestScriptProvider_Init(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
err := provider.Init()
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestScriptProvider_Cleanup(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
err := provider.Cleanup()
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestScriptProvider_RequiredCredentialFields(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
fields := provider.RequiredCredentialFields()
|
|
|
|
require.Len(t, fields, 1)
|
|
|
|
field := fields[0]
|
|
assert.Equal(t, "script_path", field.Name)
|
|
assert.Equal(t, "Script Path", field.Label)
|
|
assert.Equal(t, "text", field.Type)
|
|
assert.NotEmpty(t, field.Placeholder)
|
|
assert.Contains(t, field.Hint, "/scripts/")
|
|
}
|
|
|
|
func TestScriptProvider_OptionalCredentialFields(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
fields := provider.OptionalCredentialFields()
|
|
|
|
expectedFields := map[string]bool{
|
|
"timeout_seconds": false,
|
|
"env_vars": false,
|
|
}
|
|
|
|
assert.Len(t, fields, len(expectedFields))
|
|
|
|
for _, field := range fields {
|
|
if _, ok := expectedFields[field.Name]; !ok {
|
|
t.Errorf("Unexpected optional field: %q", field.Name)
|
|
}
|
|
expectedFields[field.Name] = true
|
|
|
|
assert.NotEmpty(t, field.Label, "Field %q has empty label", field.Name)
|
|
assert.NotEmpty(t, field.Type, "Field %q has empty type", field.Name)
|
|
|
|
// Verify env_vars is textarea type
|
|
if field.Name == "env_vars" {
|
|
assert.Equal(t, "textarea", field.Type, "env_vars should be textarea type")
|
|
}
|
|
}
|
|
|
|
for name, found := range expectedFields {
|
|
if !found {
|
|
t.Errorf("Missing optional field: %q", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScriptProvider_ValidateCredentials(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
|
|
tests := []struct {
|
|
name string
|
|
creds map[string]string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid credentials minimal",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid credentials with all options",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "120",
|
|
"env_vars": "API_KEY=secret123\nAPI_URL=https://api.example.com",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid credentials with nested directory",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns/acme/challenge.sh",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid credentials with whitespace trimming",
|
|
creds: map[string]string{
|
|
"script_path": " /scripts/dns-challenge.sh ",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "missing script_path",
|
|
creds: map[string]string{},
|
|
wantErr: true,
|
|
errMsg: "script_path is required",
|
|
},
|
|
{
|
|
name: "empty script_path",
|
|
creds: map[string]string{
|
|
"script_path": "",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script_path is required",
|
|
},
|
|
{
|
|
name: "whitespace-only script_path",
|
|
creds: map[string]string{
|
|
"script_path": " ",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script_path is required",
|
|
},
|
|
// Path traversal attacks - caught via directory prefix check after path normalization
|
|
{
|
|
name: "path traversal with ..",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/../etc/passwd",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "path traversal at end",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns/../../../etc/passwd",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "script outside allowed directory",
|
|
creds: map[string]string{
|
|
"script_path": "/etc/passwd",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "script in home directory",
|
|
creds: map[string]string{
|
|
"script_path": "/home/user/script.sh",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "script at root",
|
|
creds: map[string]string{
|
|
"script_path": "/script.sh",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "relative path",
|
|
creds: map[string]string{
|
|
"script_path": "scripts/dns-challenge.sh",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "script filename with hyphen prefix",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/-rf",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script filename cannot start with hyphen",
|
|
},
|
|
{
|
|
name: "script filename with special characters",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/script;rm -rf /",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script filename contains invalid characters",
|
|
},
|
|
{
|
|
name: "script filename with spaces",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/my script.sh",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "script filename contains invalid characters",
|
|
},
|
|
// Timeout validation
|
|
{
|
|
name: "timeout_seconds not a number",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "abc",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "timeout_seconds must be a number",
|
|
},
|
|
{
|
|
name: "timeout_seconds too low",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "1",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "timeout_seconds must be between",
|
|
},
|
|
{
|
|
name: "timeout_seconds too high",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "500",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "timeout_seconds must be between",
|
|
},
|
|
{
|
|
name: "valid min timeout",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "5",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid max timeout",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "300",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
// Environment variable validation
|
|
{
|
|
name: "env_vars invalid format no equals",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"env_vars": "INVALID_VAR",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "invalid format, expected KEY=VALUE",
|
|
},
|
|
{
|
|
name: "env_vars key starting with number",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"env_vars": "1INVALID=value",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "invalid environment variable format",
|
|
},
|
|
{
|
|
name: "env_vars override PATH",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"env_vars": "PATH=/malicious/path",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "env_vars override LD_PRELOAD",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"env_vars": "LD_PRELOAD=/malicious/lib.so",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "env_vars override HOME (case insensitive)",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"env_vars": "home=/tmp",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "env_vars with empty lines",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"env_vars": "API_KEY=secret\n\nAPI_URL=https://example.com\n",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "env_vars with underscore prefix",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"env_vars": "_PRIVATE_VAR=value",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := provider.ValidateCredentials(tt.creds)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
if tt.errMsg != "" {
|
|
assert.Contains(t, err.Error(), tt.errMsg)
|
|
}
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScriptProvider_ValidateScriptPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
scriptPath string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid path",
|
|
scriptPath: "/scripts/dns-challenge.sh",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid nested path",
|
|
scriptPath: "/scripts/acme/dns/challenge.sh",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid path with dots in filename",
|
|
scriptPath: "/scripts/dns.challenge.v1.sh",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid path with underscore",
|
|
scriptPath: "/scripts/dns_challenge.sh",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid path with hyphen",
|
|
scriptPath: "/scripts/dns-challenge.sh",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "path traversal basic",
|
|
scriptPath: "/scripts/../etc/passwd",
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "path traversal complex",
|
|
scriptPath: "/scripts/subdir/../../../etc/passwd",
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "outside allowed directory",
|
|
scriptPath: "/etc/passwd",
|
|
wantErr: true,
|
|
errMsg: "script must be in /scripts/ directory",
|
|
},
|
|
{
|
|
name: "script with hyphen prefix filename",
|
|
scriptPath: "/scripts/-help",
|
|
wantErr: true,
|
|
errMsg: "script filename cannot start with hyphen",
|
|
},
|
|
{
|
|
name: "script with shell metacharacters",
|
|
scriptPath: "/scripts/script$(whoami).sh",
|
|
wantErr: true,
|
|
errMsg: "script filename contains invalid characters",
|
|
},
|
|
{
|
|
name: "script with backtick",
|
|
scriptPath: "/scripts/script`id`.sh",
|
|
wantErr: true,
|
|
errMsg: "script filename contains invalid characters",
|
|
},
|
|
{
|
|
name: "script with pipe",
|
|
scriptPath: "/scripts/script|cat.sh",
|
|
wantErr: true,
|
|
errMsg: "script filename contains invalid characters",
|
|
},
|
|
{
|
|
name: "script with semicolon",
|
|
scriptPath: "/scripts/script;ls.sh",
|
|
wantErr: true,
|
|
errMsg: "script filename contains invalid characters",
|
|
},
|
|
{
|
|
name: "script with ampersand",
|
|
scriptPath: "/scripts/script&bg.sh",
|
|
wantErr: true,
|
|
errMsg: "script filename contains invalid characters",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateScriptPath(tt.scriptPath)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
if tt.errMsg != "" {
|
|
assert.Contains(t, err.Error(), tt.errMsg)
|
|
}
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScriptProvider_ValidateEnvVars(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
envVars string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "empty string",
|
|
envVars: "",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "single valid variable",
|
|
envVars: "API_KEY=secret123",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "multiple valid variables",
|
|
envVars: "API_KEY=secret123\nAPI_URL=https://api.example.com",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "variable with empty value",
|
|
envVars: "EMPTY_VAR=",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "variable starting with underscore",
|
|
envVars: "_PRIVATE=value",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty lines are ignored",
|
|
envVars: "VAR1=val1\n\n\nVAR2=val2",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "whitespace lines are ignored",
|
|
envVars: "VAR1=val1\n \nVAR2=val2",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "missing equals sign",
|
|
envVars: "INVALID_VAR",
|
|
wantErr: true,
|
|
errMsg: "invalid format, expected KEY=VALUE",
|
|
},
|
|
{
|
|
name: "key starting with number",
|
|
envVars: "1INVALID=value",
|
|
wantErr: true,
|
|
errMsg: "invalid environment variable format",
|
|
},
|
|
{
|
|
name: "key with special characters",
|
|
envVars: "INVALID-KEY=value",
|
|
wantErr: true,
|
|
errMsg: "invalid environment variable format",
|
|
},
|
|
{
|
|
name: "override PATH",
|
|
envVars: "PATH=/malicious",
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "override LD_PRELOAD",
|
|
envVars: "LD_PRELOAD=/lib.so",
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "override LD_LIBRARY_PATH",
|
|
envVars: "LD_LIBRARY_PATH=/lib",
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "override HOME",
|
|
envVars: "HOME=/tmp",
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "override USER",
|
|
envVars: "USER=root",
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "override SHELL",
|
|
envVars: "SHELL=/bin/bash",
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
{
|
|
name: "case insensitive critical var check",
|
|
envVars: "path=/malicious",
|
|
wantErr: true,
|
|
errMsg: "cannot override critical environment variable",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateEnvVars(tt.envVars)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
if tt.errMsg != "" {
|
|
assert.Contains(t, err.Error(), tt.errMsg)
|
|
}
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScriptProvider_ParseEnvVars(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
envVars string
|
|
expected map[string]string
|
|
}{
|
|
{
|
|
name: "empty string",
|
|
envVars: "",
|
|
expected: map[string]string{},
|
|
},
|
|
{
|
|
name: "single variable",
|
|
envVars: "API_KEY=secret123",
|
|
expected: map[string]string{
|
|
"API_KEY": "secret123",
|
|
},
|
|
},
|
|
{
|
|
name: "multiple variables",
|
|
envVars: "API_KEY=secret123\nAPI_URL=https://api.example.com",
|
|
expected: map[string]string{
|
|
"API_KEY": "secret123",
|
|
"API_URL": "https://api.example.com",
|
|
},
|
|
},
|
|
{
|
|
name: "variable with empty value",
|
|
envVars: "EMPTY_VAR=",
|
|
expected: map[string]string{
|
|
"EMPTY_VAR": "",
|
|
},
|
|
},
|
|
{
|
|
name: "variable with equals in value",
|
|
envVars: "CONNECTION=host=localhost;port=5432",
|
|
expected: map[string]string{
|
|
"CONNECTION": "host=localhost;port=5432",
|
|
},
|
|
},
|
|
{
|
|
name: "skip empty lines",
|
|
envVars: "VAR1=val1\n\nVAR2=val2",
|
|
expected: map[string]string{
|
|
"VAR1": "val1",
|
|
"VAR2": "val2",
|
|
},
|
|
},
|
|
{
|
|
name: "trim whitespace",
|
|
envVars: " VAR1=val1 \n VAR2=val2 ",
|
|
expected: map[string]string{
|
|
"VAR1": "val1",
|
|
"VAR2": "val2",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := parseEnvVars(tt.envVars)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScriptProvider_TestCredentials(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
|
|
// TestCredentials should behave the same as ValidateCredentials
|
|
validCreds := map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
}
|
|
|
|
err := provider.TestCredentials(validCreds)
|
|
assert.NoError(t, err)
|
|
|
|
invalidCreds := map[string]string{
|
|
"script_path": "/etc/passwd",
|
|
}
|
|
|
|
err = provider.TestCredentials(invalidCreds)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestScriptProvider_SupportsMultiCredential(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
assert.False(t, provider.SupportsMultiCredential())
|
|
}
|
|
|
|
func TestScriptProvider_BuildCaddyConfig(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
|
|
tests := []struct {
|
|
name string
|
|
creds map[string]string
|
|
expected map[string]any
|
|
}{
|
|
{
|
|
name: "minimal config with defaults",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
},
|
|
expected: map[string]any{
|
|
"name": "script",
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": ScriptDefaultTimeoutSeconds,
|
|
"env_vars": map[string]string{},
|
|
},
|
|
},
|
|
{
|
|
name: "full config with all options",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "120",
|
|
"env_vars": "API_KEY=secret\nAPI_URL=https://example.com",
|
|
},
|
|
expected: map[string]any{
|
|
"name": "script",
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": 120,
|
|
"env_vars": map[string]string{
|
|
"API_KEY": "secret",
|
|
"API_URL": "https://example.com",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "whitespace trimming",
|
|
creds: map[string]string{
|
|
"script_path": " /scripts/dns-challenge.sh ",
|
|
"timeout_seconds": " 90 ",
|
|
},
|
|
expected: map[string]any{
|
|
"name": "script",
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": 90,
|
|
"env_vars": map[string]string{},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid timeout falls back to default",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "invalid",
|
|
},
|
|
expected: map[string]any{
|
|
"name": "script",
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": ScriptDefaultTimeoutSeconds,
|
|
"env_vars": map[string]string{},
|
|
},
|
|
},
|
|
{
|
|
name: "out-of-range timeout falls back to default",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": "1000",
|
|
},
|
|
expected: map[string]any{
|
|
"name": "script",
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": ScriptDefaultTimeoutSeconds,
|
|
"env_vars": map[string]string{},
|
|
},
|
|
},
|
|
{
|
|
name: "path normalization",
|
|
creds: map[string]string{
|
|
"script_path": "/scripts/./subdir/../dns-challenge.sh",
|
|
},
|
|
expected: map[string]any{
|
|
"name": "script",
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
"timeout_seconds": ScriptDefaultTimeoutSeconds,
|
|
"env_vars": map[string]string{},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := provider.BuildCaddyConfig(tt.creds)
|
|
|
|
for key, expectedValue := range tt.expected {
|
|
actualValue, ok := config[key]
|
|
if !ok {
|
|
t.Errorf("BuildCaddyConfig() missing key %q", key)
|
|
continue
|
|
}
|
|
assert.Equal(t, expectedValue, actualValue, "BuildCaddyConfig()[%q] mismatch", key)
|
|
}
|
|
|
|
// Check no unexpected keys
|
|
for key := range config {
|
|
if _, ok := tt.expected[key]; !ok {
|
|
t.Errorf("BuildCaddyConfig() unexpected key %q", key)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScriptProvider_BuildCaddyConfigForZone(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
|
|
creds := map[string]string{
|
|
"script_path": "/scripts/dns-challenge.sh",
|
|
}
|
|
|
|
config := provider.BuildCaddyConfigForZone("example.org", creds)
|
|
|
|
// Should return same as BuildCaddyConfig since multi-credential is not supported
|
|
assert.Equal(t, "script", config["name"])
|
|
assert.Equal(t, "/scripts/dns-challenge.sh", config["script_path"])
|
|
}
|
|
|
|
func TestScriptProvider_PropagationTimeout(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
timeout := provider.PropagationTimeout()
|
|
|
|
assert.Equal(t, 120*time.Second, timeout)
|
|
}
|
|
|
|
func TestScriptProvider_PollingInterval(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
interval := provider.PollingInterval()
|
|
|
|
assert.Equal(t, 5*time.Second, interval)
|
|
}
|
|
|
|
func TestScriptProvider_GetTimeoutSeconds(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
|
|
tests := []struct {
|
|
name string
|
|
creds map[string]string
|
|
expected int
|
|
}{
|
|
{
|
|
name: "empty creds returns default",
|
|
creds: map[string]string{},
|
|
expected: ScriptDefaultTimeoutSeconds,
|
|
},
|
|
{
|
|
name: "valid timeout returns value",
|
|
creds: map[string]string{"timeout_seconds": "90"},
|
|
expected: 90,
|
|
},
|
|
{
|
|
name: "invalid number returns default",
|
|
creds: map[string]string{"timeout_seconds": "abc"},
|
|
expected: ScriptDefaultTimeoutSeconds,
|
|
},
|
|
{
|
|
name: "out of range low returns default",
|
|
creds: map[string]string{"timeout_seconds": "1"},
|
|
expected: ScriptDefaultTimeoutSeconds,
|
|
},
|
|
{
|
|
name: "out of range high returns default",
|
|
creds: map[string]string{"timeout_seconds": "500"},
|
|
expected: ScriptDefaultTimeoutSeconds,
|
|
},
|
|
{
|
|
name: "min value returns value",
|
|
creds: map[string]string{"timeout_seconds": "5"},
|
|
expected: 5,
|
|
},
|
|
{
|
|
name: "max value returns value",
|
|
creds: map[string]string{"timeout_seconds": "300"},
|
|
expected: 300,
|
|
},
|
|
{
|
|
name: "with whitespace",
|
|
creds: map[string]string{"timeout_seconds": " 60 "},
|
|
expected: 60,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := provider.GetTimeoutSeconds(tt.creds)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScriptProvider_GetEnvVars(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
|
|
tests := []struct {
|
|
name string
|
|
creds map[string]string
|
|
expected map[string]string
|
|
}{
|
|
{
|
|
name: "empty creds returns empty map",
|
|
creds: map[string]string{},
|
|
expected: map[string]string{},
|
|
},
|
|
{
|
|
name: "empty env_vars returns empty map",
|
|
creds: map[string]string{"env_vars": ""},
|
|
expected: map[string]string{},
|
|
},
|
|
{
|
|
name: "single variable",
|
|
creds: map[string]string{"env_vars": "API_KEY=secret"},
|
|
expected: map[string]string{
|
|
"API_KEY": "secret",
|
|
},
|
|
},
|
|
{
|
|
name: "multiple variables",
|
|
creds: map[string]string{"env_vars": "KEY1=val1\nKEY2=val2"},
|
|
expected: map[string]string{
|
|
"KEY1": "val1",
|
|
"KEY2": "val2",
|
|
},
|
|
},
|
|
{
|
|
name: "whitespace only returns empty map",
|
|
creds: map[string]string{"env_vars": " \n "},
|
|
expected: map[string]string{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := provider.GetEnvVars(tt.creds)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScriptProvider_Constants(t *testing.T) {
|
|
// Verify constant values are sensible
|
|
assert.Equal(t, 60, ScriptDefaultTimeoutSeconds)
|
|
assert.Equal(t, 120*time.Second, ScriptDefaultPropagationTimeout)
|
|
assert.Equal(t, 5*time.Second, ScriptDefaultPollingInterval)
|
|
assert.Equal(t, 5, ScriptMinTimeoutSeconds)
|
|
assert.Equal(t, 300, ScriptMaxTimeoutSeconds)
|
|
assert.Equal(t, "/scripts/", ScriptAllowedDirectory)
|
|
|
|
// Ensure min < default < max for timeout
|
|
assert.Less(t, ScriptMinTimeoutSeconds, ScriptDefaultTimeoutSeconds)
|
|
assert.Less(t, ScriptDefaultTimeoutSeconds, ScriptMaxTimeoutSeconds)
|
|
}
|
|
|
|
func TestScriptProvider_ImplementsInterface(t *testing.T) {
|
|
provider := NewScriptProvider()
|
|
|
|
// Compile-time check that ScriptProvider implements ProviderPlugin
|
|
var _ dnsprovider.ProviderPlugin = provider
|
|
}
|
|
|
|
func TestScriptProvider_SecurityPatterns(t *testing.T) {
|
|
// Test the regex patterns used for security validation
|
|
|
|
t.Run("scriptArgPattern", func(t *testing.T) {
|
|
validNames := []string{
|
|
"script.sh",
|
|
"dns-challenge.sh",
|
|
"dns_challenge_v1.sh",
|
|
"CHALLENGE.SH",
|
|
"script123.sh",
|
|
"a.b.c.d.sh",
|
|
}
|
|
|
|
for _, name := range validNames {
|
|
assert.True(t, scriptArgPattern.MatchString(name), "Expected %q to match", name)
|
|
}
|
|
|
|
invalidNames := []string{
|
|
"script$(id).sh",
|
|
"script`id`.sh",
|
|
"script;rm.sh",
|
|
"script|cat.sh",
|
|
"script&bg.sh",
|
|
"script>out.sh",
|
|
"script<in.sh",
|
|
"script\\n.sh",
|
|
"script .sh",
|
|
"script\t.sh",
|
|
"script'quote.sh",
|
|
"script\"quote.sh",
|
|
}
|
|
|
|
for _, name := range invalidNames {
|
|
assert.False(t, scriptArgPattern.MatchString(name), "Expected %q to NOT match", name)
|
|
}
|
|
})
|
|
|
|
t.Run("envVarLinePattern", func(t *testing.T) {
|
|
validLines := []string{
|
|
"VAR=value",
|
|
"_VAR=value",
|
|
"VAR123=value",
|
|
"_VAR_123=value",
|
|
"VAR=",
|
|
"VAR=value=with=equals",
|
|
"var=lowercase",
|
|
}
|
|
|
|
for _, line := range validLines {
|
|
assert.True(t, envVarLinePattern.MatchString(line), "Expected %q to match", line)
|
|
}
|
|
|
|
invalidLines := []string{
|
|
"123VAR=value",
|
|
"-VAR=value",
|
|
"VAR-NAME=value",
|
|
"VAR.NAME=value",
|
|
"VAR NAME=value",
|
|
}
|
|
|
|
for _, line := range invalidLines {
|
|
assert.False(t, envVarLinePattern.MatchString(line), "Expected %q to NOT match", line)
|
|
}
|
|
})
|
|
}
|