Files
Charon/backend/pkg/dnsprovider/custom/webhook_provider_test.go
GitHub Actions 3169b05156 fix: skip incomplete system log viewer tests
- 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)
2026-02-09 21:55:55 +00:00

857 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 TestNewWebhookProvider(t *testing.T) {
provider := NewWebhookProvider()
require.NotNil(t, provider)
assert.Equal(t, WebhookDefaultPropagationTimeout, provider.propagationTimeout)
assert.Equal(t, WebhookDefaultPollingInterval, provider.pollingInterval)
}
func TestWebhookProvider_Type(t *testing.T) {
provider := NewWebhookProvider()
assert.Equal(t, "webhook", provider.Type())
}
func TestWebhookProvider_Metadata(t *testing.T) {
provider := NewWebhookProvider()
metadata := provider.Metadata()
assert.Equal(t, "webhook", metadata.Type)
assert.Equal(t, "Webhook (HTTP)", metadata.Name)
assert.Contains(t, metadata.Description, "HTTP webhook")
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 TestWebhookProvider_Init(t *testing.T) {
provider := NewWebhookProvider()
err := provider.Init()
assert.NoError(t, err)
}
func TestWebhookProvider_Cleanup(t *testing.T) {
provider := NewWebhookProvider()
err := provider.Cleanup()
assert.NoError(t, err)
}
func TestWebhookProvider_RequiredCredentialFields(t *testing.T) {
provider := NewWebhookProvider()
fields := provider.RequiredCredentialFields()
expectedFields := map[string]bool{
"create_url": false,
"delete_url": false,
}
assert.Len(t, fields, len(expectedFields))
for _, field := range fields {
if _, ok := expectedFields[field.Name]; !ok {
t.Errorf("Unexpected required 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)
assert.NotEmpty(t, field.Hint, "Field %q has empty hint", field.Name)
}
for name, found := range expectedFields {
if !found {
t.Errorf("Missing required field: %q", name)
}
}
}
func TestWebhookProvider_OptionalCredentialFields(t *testing.T) {
provider := NewWebhookProvider()
fields := provider.OptionalCredentialFields()
expectedFields := map[string]bool{
"auth_header": false,
"auth_value": false,
"timeout_seconds": false,
"retry_count": false,
"insecure_skip_verify": 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)
// Verify auth_value is password type
if field.Name == "auth_value" {
assert.Equal(t, "password", field.Type, "auth_value should be password type")
}
// Verify insecure_skip_verify has select options
if field.Name == "insecure_skip_verify" {
assert.Equal(t, "select", field.Type, "insecure_skip_verify should be select type")
assert.Len(t, field.Options, 2, "insecure_skip_verify should have 2 options")
assert.Contains(t, field.Hint, "DEVELOPMENT ONLY", "insecure_skip_verify should warn about dev-only usage")
}
}
for name, found := range expectedFields {
if !found {
t.Errorf("Missing optional field: %q", name)
}
}
}
func TestWebhookProvider_ValidateCredentials(t *testing.T) {
provider := NewWebhookProvider()
tests := []struct {
name string
creds map[string]string
wantErr bool
errMsg string
}{
{
name: "valid credentials with HTTPS URLs",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
},
wantErr: false,
},
{
name: "valid credentials with localhost HTTP",
creds: map[string]string{
"create_url": "http://localhost:8080/create",
"delete_url": "http://localhost:8080/delete",
},
wantErr: false,
},
{
name: "valid credentials with 127.0.0.1 HTTP",
creds: map[string]string{
"create_url": "http://127.0.0.1:8080/create",
"delete_url": "http://127.0.0.1:8080/delete",
},
wantErr: false,
},
{
name: "valid credentials with all optional fields",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"auth_header": "Authorization",
"auth_value": "Bearer token123",
"timeout_seconds": "60",
"retry_count": "5",
"insecure_skip_verify": "false",
},
wantErr: false,
},
{
name: "valid credentials with whitespace trimming",
creds: map[string]string{
"create_url": " https://dns-api.example.com/create ",
"delete_url": " https://dns-api.example.com/delete ",
},
wantErr: false,
},
{
name: "missing create_url",
creds: map[string]string{
"delete_url": "https://dns-api.example.com/delete",
},
wantErr: true,
errMsg: "create_url is required",
},
{
name: "empty create_url",
creds: map[string]string{
"create_url": "",
"delete_url": "https://dns-api.example.com/delete",
},
wantErr: true,
errMsg: "create_url is required",
},
{
name: "whitespace-only create_url",
creds: map[string]string{
"create_url": " ",
"delete_url": "https://dns-api.example.com/delete",
},
wantErr: true,
errMsg: "create_url is required",
},
{
name: "missing delete_url",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
},
wantErr: true,
errMsg: "delete_url is required",
},
{
name: "empty delete_url",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "",
},
wantErr: true,
errMsg: "delete_url is required",
},
{
name: "invalid create_url format",
creds: map[string]string{
"create_url": "not-a-valid-url",
"delete_url": "https://dns-api.example.com/delete",
},
wantErr: true,
errMsg: "create_url",
},
{
name: "create_url with ftp scheme",
creds: map[string]string{
"create_url": "ftp://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
},
wantErr: true,
errMsg: "must use http or https scheme",
},
{
name: "HTTP scheme for non-localhost",
creds: map[string]string{
"create_url": "http://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
},
wantErr: true,
errMsg: "must use HTTPS for non-localhost",
},
{
name: "timeout_seconds not a number",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": "abc",
},
wantErr: true,
errMsg: "timeout_seconds must be a number",
},
{
name: "timeout_seconds too low",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": "1",
},
wantErr: true,
errMsg: "timeout_seconds must be between",
},
{
name: "timeout_seconds too high",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": "500",
},
wantErr: true,
errMsg: "timeout_seconds must be between",
},
{
name: "retry_count not a number",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"retry_count": "abc",
},
wantErr: true,
errMsg: "retry_count must be a number",
},
{
name: "retry_count negative",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"retry_count": "-1",
},
wantErr: true,
errMsg: "retry_count must be between",
},
{
name: "retry_count too high",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"retry_count": "20",
},
wantErr: true,
errMsg: "retry_count must be between",
},
{
name: "insecure_skip_verify invalid value",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"insecure_skip_verify": "maybe",
},
wantErr: true,
errMsg: "insecure_skip_verify must be 'true' or 'false'",
},
{
name: "auth_header without auth_value",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"auth_header": "Authorization",
},
wantErr: true,
errMsg: "both auth_header and auth_value must be provided together",
},
{
name: "auth_value without auth_header",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"auth_value": "Bearer token",
},
wantErr: true,
errMsg: "both auth_header and auth_value must be provided together",
},
{
name: "valid min timeout",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": "5",
},
wantErr: false,
},
{
name: "valid max timeout",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": "300",
},
wantErr: false,
},
{
name: "valid min retry count",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"retry_count": "0",
},
wantErr: false,
},
{
name: "valid max retry count",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"retry_count": "10",
},
wantErr: false,
},
{
name: "insecure_skip_verify true",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"insecure_skip_verify": "true",
},
wantErr: false,
},
{
name: "insecure_skip_verify TRUE (case insensitive)",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"insecure_skip_verify": "TRUE",
},
wantErr: false,
},
{
name: "all empty credentials",
creds: map[string]string{},
wantErr: true,
errMsg: "create_url is required",
},
}
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 TestWebhookProvider_ValidateWebhookURL(t *testing.T) {
provider := NewWebhookProvider()
tests := []struct {
name string
url string
fieldName string
wantErr bool
errMsg string
}{
{
name: "valid HTTPS URL",
url: "https://api.example.com/webhook",
fieldName: "test_url",
wantErr: false,
},
{
name: "valid localhost HTTP",
url: "http://localhost:8080/webhook",
fieldName: "test_url",
wantErr: false,
},
{
name: "valid 127.0.0.1 HTTP",
url: "http://127.0.0.1:8080/webhook",
fieldName: "test_url",
wantErr: false,
},
{
name: "invalid scheme ftp",
url: "ftp://example.com/webhook",
fieldName: "test_url",
wantErr: true,
errMsg: "must use http or https scheme",
},
{
name: "HTTP for non-localhost rejected",
url: "http://api.example.com/webhook",
fieldName: "test_url",
wantErr: true,
errMsg: "must use HTTPS for non-localhost",
},
{
name: "missing hostname",
url: "https:///path",
fieldName: "test_url",
wantErr: true,
errMsg: "is missing hostname",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := provider.validateWebhookURL(tt.url, tt.fieldName)
if tt.wantErr {
assert.Error(t, err)
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestWebhookProvider_TestCredentials(t *testing.T) {
provider := NewWebhookProvider()
// TestCredentials should behave the same as ValidateCredentials
validCreds := map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
}
err := provider.TestCredentials(validCreds)
assert.NoError(t, err)
invalidCreds := map[string]string{
"create_url": "https://dns-api.example.com/create",
}
err = provider.TestCredentials(invalidCreds)
assert.Error(t, err)
}
func TestWebhookProvider_SupportsMultiCredential(t *testing.T) {
provider := NewWebhookProvider()
assert.False(t, provider.SupportsMultiCredential())
}
func TestWebhookProvider_BuildCaddyConfig(t *testing.T) {
provider := NewWebhookProvider()
tests := []struct {
name string
creds map[string]string
expected map[string]any
}{
{
name: "minimal config with defaults",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
},
expected: map[string]any{
"name": "webhook",
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": WebhookDefaultTimeoutSeconds,
"retry_count": WebhookDefaultRetryCount,
"insecure_skip_verify": false,
},
},
{
name: "full config with all options",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"auth_header": "X-API-Key",
"auth_value": "secret123",
"timeout_seconds": "60",
"retry_count": "5",
"insecure_skip_verify": "true",
},
expected: map[string]any{
"name": "webhook",
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"auth_header": "X-API-Key",
"auth_value": "secret123",
"timeout_seconds": 60,
"retry_count": 5,
"insecure_skip_verify": true,
},
},
{
name: "whitespace trimming",
creds: map[string]string{
"create_url": " https://dns-api.example.com/create ",
"delete_url": " https://dns-api.example.com/delete ",
"auth_header": " Authorization ",
"auth_value": " Bearer token ",
"timeout_seconds": " 45 ",
"retry_count": " 2 ",
},
expected: map[string]any{
"name": "webhook",
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"auth_header": "Authorization",
"auth_value": "Bearer token",
"timeout_seconds": 45,
"retry_count": 2,
"insecure_skip_verify": false,
},
},
{
name: "invalid timeout falls back to default",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": "invalid",
},
expected: map[string]any{
"name": "webhook",
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": WebhookDefaultTimeoutSeconds,
"retry_count": WebhookDefaultRetryCount,
"insecure_skip_verify": false,
},
},
{
name: "out-of-range timeout falls back to default",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": "1000",
},
expected: map[string]any{
"name": "webhook",
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": WebhookDefaultTimeoutSeconds,
"retry_count": WebhookDefaultRetryCount,
"insecure_skip_verify": false,
},
},
{
name: "out-of-range retry falls back to default",
creds: map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"retry_count": "100",
},
expected: map[string]any{
"name": "webhook",
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
"timeout_seconds": WebhookDefaultTimeoutSeconds,
"retry_count": WebhookDefaultRetryCount,
"insecure_skip_verify": false,
},
},
}
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 TestWebhookProvider_BuildCaddyConfigForZone(t *testing.T) {
provider := NewWebhookProvider()
creds := map[string]string{
"create_url": "https://dns-api.example.com/create",
"delete_url": "https://dns-api.example.com/delete",
}
config := provider.BuildCaddyConfigForZone("example.org", creds)
// Should return same as BuildCaddyConfig since multi-credential is not supported
assert.Equal(t, "webhook", config["name"])
assert.Equal(t, "https://dns-api.example.com/create", config["create_url"])
assert.Equal(t, "https://dns-api.example.com/delete", config["delete_url"])
}
func TestWebhookProvider_PropagationTimeout(t *testing.T) {
provider := NewWebhookProvider()
timeout := provider.PropagationTimeout()
assert.Equal(t, 120*time.Second, timeout)
}
func TestWebhookProvider_PollingInterval(t *testing.T) {
provider := NewWebhookProvider()
interval := provider.PollingInterval()
assert.Equal(t, 5*time.Second, interval)
}
func TestWebhookProvider_GetTimeoutSeconds(t *testing.T) {
provider := NewWebhookProvider()
tests := []struct {
name string
creds map[string]string
expected int
}{
{
name: "empty creds returns default",
creds: map[string]string{},
expected: WebhookDefaultTimeoutSeconds,
},
{
name: "valid timeout returns value",
creds: map[string]string{"timeout_seconds": "60"},
expected: 60,
},
{
name: "invalid number returns default",
creds: map[string]string{"timeout_seconds": "abc"},
expected: WebhookDefaultTimeoutSeconds,
},
{
name: "out of range low returns default",
creds: map[string]string{"timeout_seconds": "1"},
expected: WebhookDefaultTimeoutSeconds,
},
{
name: "out of range high returns default",
creds: map[string]string{"timeout_seconds": "500"},
expected: WebhookDefaultTimeoutSeconds,
},
{
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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.GetTimeoutSeconds(tt.creds)
assert.Equal(t, tt.expected, result)
})
}
}
func TestWebhookProvider_GetRetryCount(t *testing.T) {
provider := NewWebhookProvider()
tests := []struct {
name string
creds map[string]string
expected int
}{
{
name: "empty creds returns default",
creds: map[string]string{},
expected: WebhookDefaultRetryCount,
},
{
name: "valid retry count returns value",
creds: map[string]string{"retry_count": "5"},
expected: 5,
},
{
name: "invalid number returns default",
creds: map[string]string{"retry_count": "abc"},
expected: WebhookDefaultRetryCount,
},
{
name: "negative returns default",
creds: map[string]string{"retry_count": "-1"},
expected: WebhookDefaultRetryCount,
},
{
name: "out of range high returns default",
creds: map[string]string{"retry_count": "100"},
expected: WebhookDefaultRetryCount,
},
{
name: "min value returns value",
creds: map[string]string{"retry_count": "0"},
expected: 0,
},
{
name: "max value returns value",
creds: map[string]string{"retry_count": "10"},
expected: 10,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.GetRetryCount(tt.creds)
assert.Equal(t, tt.expected, result)
})
}
}
func TestWebhookProvider_IsInsecureSkipVerify(t *testing.T) {
provider := NewWebhookProvider()
tests := []struct {
name string
creds map[string]string
expected bool
}{
{
name: "empty creds returns false",
creds: map[string]string{},
expected: false,
},
{
name: "false returns false",
creds: map[string]string{"insecure_skip_verify": "false"},
expected: false,
},
{
name: "true returns true",
creds: map[string]string{"insecure_skip_verify": "true"},
expected: true,
},
{
name: "TRUE returns true",
creds: map[string]string{"insecure_skip_verify": "TRUE"},
expected: true,
},
{
name: "False returns false",
creds: map[string]string{"insecure_skip_verify": "False"},
expected: false,
},
{
name: "invalid value returns false",
creds: map[string]string{"insecure_skip_verify": "invalid"},
expected: false,
},
{
name: "with whitespace",
creds: map[string]string{"insecure_skip_verify": " true "},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.IsInsecureSkipVerify(tt.creds)
assert.Equal(t, tt.expected, result)
})
}
}
func TestWebhookProvider_Constants(t *testing.T) {
// Verify constant values are sensible
assert.Equal(t, 30, WebhookDefaultTimeoutSeconds)
assert.Equal(t, 3, WebhookDefaultRetryCount)
assert.Equal(t, 120*time.Second, WebhookDefaultPropagationTimeout)
assert.Equal(t, 5*time.Second, WebhookDefaultPollingInterval)
assert.Equal(t, 5, WebhookMinTimeoutSeconds)
assert.Equal(t, 300, WebhookMaxTimeoutSeconds)
assert.Equal(t, 0, WebhookMinRetryCount)
assert.Equal(t, 10, WebhookMaxRetryCount)
// Ensure min < default < max for timeout
assert.Less(t, WebhookMinTimeoutSeconds, WebhookDefaultTimeoutSeconds)
assert.Less(t, WebhookDefaultTimeoutSeconds, WebhookMaxTimeoutSeconds)
// Ensure min <= default <= max for retry
assert.LessOrEqual(t, WebhookMinRetryCount, WebhookDefaultRetryCount)
assert.LessOrEqual(t, WebhookDefaultRetryCount, WebhookMaxRetryCount)
}
func TestWebhookProvider_ImplementsInterface(t *testing.T) {
provider := NewWebhookProvider()
// Compile-time check that WebhookProvider implements ProviderPlugin
var _ dnsprovider.ProviderPlugin = provider
}