Files
Charon/docs/plans/current_spec.md
GitHub Actions 5bafd92edf fix: supply slack webhook token in handler create sub-tests
The slack sub-tests in TestDiscordOnly_CreateRejectsNonDiscord and
TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents were
omitting the required token field from their request payloads.
CreateProvider enforces that Slack providers must have a non-empty
token (the webhook URL) at creation time. Without it the service
returns "slack webhook URL is required", which the handler does not
classify as a 400 validation error, so it falls through to 500.

Add a token field to each test struct, populate it for the slack
case with a valid-format Slack webhook URL, and use
WithSlackURLValidator to bypass the real format check in unit tests —
matching the pattern used in all existing service-level Slack tests.
2026-03-15 15:17:23 +00:00

11 KiB

Fix Plan: Slack Sub-Test Failures in Handler-Level Provider Create Tests

Status: Active Created: 2026-03-15 Branch: feature/beta-release Scope: Two failing go test sub-tests in the handlers package Previous Plan: Backed up to docs/plans/current_spec.md.bak


1. Failing Tests

Test File Sub-test Expected Actual
TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents notification_provider_blocker3_test.go slack 201 Created 500 Internal Server Error
TestDiscordOnly_CreateRejectsNonDiscord notification_provider_discord_only_test.go slack 201 Created 500 Internal Server Error

2. Root Cause Analysis

2.1 Execution Path

Both tests are table-driven. For the slack row, the request payload is built as:

payload := map[string]interface{}{
    "name":    "Test Provider",
    "type":    "slack",
    "url":     "https://example.com/webhook",
    "enabled": true,
    // ... (no "token" field)
}

The handler Create() in notification_provider_handler.go passes the bound request to service.CreateProvider().

2.2 Service Enforcement

CreateProvider() in notification_service.go (line ~793) has a mandatory token guard for Slack:

if provider.Type == "slack" {
    token := strings.TrimSpace(provider.Token)
    if token == "" {
        return fmt.Errorf("slack webhook URL is required")  // ← triggered
    }
    if err := s.validateSlackURL(token); err != nil {
        return err
    }
}

Because the test payload omits "token", provider.Token is empty, and CreateProvider returns the error "slack webhook URL is required".

2.3 Handler Error Classifier Gap

The handler passes the error to isProviderValidationError() (line ~280):

func isProviderValidationError(err error) bool {
    errMsg := err.Error()
    return strings.Contains(errMsg, "invalid custom template") ||
        strings.Contains(errMsg, "rendered template") ||
        strings.Contains(errMsg, "failed to parse template") ||
        strings.Contains(errMsg, "failed to render template") ||
        strings.Contains(errMsg, "invalid Discord webhook URL") ||
        strings.Contains(errMsg, "invalid Slack webhook URL")
}

The string "slack webhook URL is required" matches none of these patterns. The handler falls through to the default 500 branch:

respondSanitizedProviderError(c, http.StatusInternalServerError, "PROVIDER_CREATE_FAILED", "internal", "Failed to create provider")

2.4 Summary

The production code is correct. Slack providers must have a webhook URL (stored in token) at creation time. The Slack webhook URL is architecturally separate from url — it is a credential/secret and is stored in token, not url. The tests are wrong because they supply a slack provider type with no token field, which the service correctly rejects. The 500 response is a side-effect of isProviderValidationError not classifying "slack webhook URL is required" as a 400-class validation error, but that is a separate concern; the fix belongs in the tests.


3. Files Involved

Production (no changes needed)

File Function Role
backend/internal/api/handlers/notification_provider_handler.go Create() Binds request, calls service, returns HTTP response
backend/internal/api/handlers/notification_provider_handler.go isProviderValidationError() Classifies service errors for 400 vs 500 routing
backend/internal/services/notification_service.go CreateProvider() Enforces Slack token requirement at line ~793
backend/internal/services/notification_service.go validateSlackWebhookURL() Validates https://hooks.slack.com/services/T.../B.../xxx regex
backend/internal/services/notification_service.go WithSlackURLValidator() Service option to override URL validator in tests

Tests (require fixes)

File Test Change
backend/internal/api/handlers/notification_provider_discord_only_test.go TestDiscordOnly_CreateRejectsNonDiscord Add token to struct + payload; use WithSlackURLValidator
backend/internal/api/handlers/notification_provider_blocker3_test.go TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents Add token to struct + payload; use WithSlackURLValidator

4. Minimal Fix

No production code changes are required. All changes are confined to the two test files.

4.1 Established Pattern

The services package already uses WithSlackURLValidator in 7 tests to bypass the real URL format check in unit tests (notification_service_test.go lines 1456, 1482, 1507, 3304, 3334, 3370, 3397 and notification_service_json_test.go line 196). The handler tests adopt the same pattern.

4.2 Fix for notification_provider_discord_only_test.go

Change 1 — service constructor (line ~27):

// Before
service := services.NewNotificationService(db, nil)

// After
service := services.NewNotificationService(db, nil,
    services.WithSlackURLValidator(func(string) error { return nil }),
)

Change 2 — test struct (line ~33):

// Before
testCases := []struct {
    name         string
    providerType string
    wantStatus   int
    wantCode     string
}{
    {"webhook",  "webhook",  http.StatusCreated,    ""},
    {"gotify",   "gotify",   http.StatusCreated,    ""},
    {"slack",    "slack",    http.StatusCreated,    ""},
    {"telegram", "telegram", http.StatusCreated,    ""},
    {"generic",  "generic",  http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE"},
    {"email",    "email",    http.StatusCreated,    ""},
}

// After
testCases := []struct {
    name         string
    providerType string
    token        string
    wantStatus   int
    wantCode     string
}{
    {"webhook",  "webhook",  "",                                                                              http.StatusCreated,    ""},
    {"gotify",   "gotify",   "",                                                                              http.StatusCreated,    ""},
    {"slack",    "slack",    "https://hooks.slack.com/services/T1234567890/B1234567890/XXXXXXXXXXXXXXXXXXXX", http.StatusCreated,    ""},
    {"telegram", "telegram", "",                                                                              http.StatusCreated,    ""},
    {"generic",  "generic",  "",                                                                              http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE"},
    {"email",    "email",    "",                                                                              http.StatusCreated,    ""},
}

Change 3 — payload map (line ~48):

// Before
payload := map[string]interface{}{
    "name":               "Test Provider",
    "type":               tc.providerType,
    "url":                "https://example.com/webhook",
    "enabled":            true,
    "notify_proxy_hosts": true,
}

// After
payload := map[string]interface{}{
    "name":               "Test Provider",
    "type":               tc.providerType,
    "url":                "https://example.com/webhook",
    "token":              tc.token,
    "enabled":            true,
    "notify_proxy_hosts": true,
}

4.3 Fix for notification_provider_blocker3_test.go

Change 1 — service constructor (line ~32):

// Before
service := services.NewNotificationService(db, nil)

// After
service := services.NewNotificationService(db, nil,
    services.WithSlackURLValidator(func(string) error { return nil }),
)

Change 2 — test struct (line ~35):

// Before
testCases := []struct {
    name         string
    providerType string
    wantStatus   int
}{
    {"webhook", "webhook", http.StatusCreated},
    {"gotify",  "gotify",  http.StatusCreated},
    {"slack",   "slack",   http.StatusCreated},
    {"email",   "email",   http.StatusCreated},
}

// After
testCases := []struct {
    name         string
    providerType string
    token        string
    wantStatus   int
}{
    {"webhook", "webhook", "",                                                                              http.StatusCreated},
    {"gotify",  "gotify",  "",                                                                              http.StatusCreated},
    {"slack",   "slack",   "https://hooks.slack.com/services/T1234567890/B1234567890/XXXXXXXXXXXXXXXXXXXX", http.StatusCreated},
    {"email",   "email",   "",                                                                              http.StatusCreated},
}

Change 3 — payload map (line ~50):

// Before
payload := map[string]interface{}{
    "name":                       "Test Provider",
    "type":                       tc.providerType,
    "url":                        "https://example.com/webhook",
    "enabled":                    true,
    "notify_security_waf_blocks": true,
}

// After
payload := map[string]interface{}{
    "name":                       "Test Provider",
    "type":                       tc.providerType,
    "url":                        "https://example.com/webhook",
    "token":                      tc.token,
    "enabled":                    true,
    "notify_security_waf_blocks": true,
}

5. What Is Not Changing

  • notification_provider_handler.goCreate() and isProviderValidationError() are correct as written.
  • notification_service.go — The Slack token requirement in CreateProvider() is correct behavior; Slack webhook URLs are secrets and belong in token, not url.
  • No new tests — Existing test coverage is sufficient. The tests just need correct input data that reflects the real API contract.

6. Commit Slicing Strategy

A single commit and single PR is appropriate. The entire change is two test files, three small hunks each (service constructor, struct definition, payload map). There are no production code changes and no risk of merge conflicts.

Suggested commit message:

test: supply slack webhook token in handler create sub-tests

The slack sub-tests in TestDiscordOnly_CreateRejectsNonDiscord and
TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents were
omitting the required token field from their request payloads.
CreateProvider enforces that Slack providers must have a non-empty
token (the webhook URL) at creation time. Without it the service
returns "slack webhook URL is required", which the handler does not
classify as a 400 validation error, so it falls through to 500.

Add a token field to each test struct, populate it for the slack
case with a valid-format Slack webhook URL, and use
WithSlackURLValidator to bypass the real format check in unit tests —
matching the pattern used in all existing service-level Slack tests.

7. Verification

After applying the fix, run the two previously failing sub-tests:

cd /projects/Charon/backend
go test ./internal/api/handlers/... \
  -run "TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents/slack|TestDiscordOnly_CreateRejectsNonDiscord/slack" \
  -v

Both sub-tests must exit PASS. Then run the full handlers suite to confirm no regressions:

go test ./internal/api/handlers/... -count=1