Files
ITWorx-Pulse-Public/internal/notification/webhook_test.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

145 lines
5.3 KiB
Go

package notification
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
func validDelivery() Delivery {
return Delivery{ID: "delivery-1", IdempotencyKey: "alert:disk-1:firing", EventType: EventFiring, Subject: "Schijfwaarschuwing", Body: "Disk 1 is bijna vol.", Attempt: 1}
}
func TestWebhookSenderDeliversBoundedAuthenticatedPayload(t *testing.T) {
var received webhookPayload
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Header.Get("Authorization") != "Bearer runtime-secret" {
t.Errorf("authorization = %q", request.Header.Get("Authorization"))
}
if request.Header.Get("Idempotency-Key") != "alert:disk-1:firing" {
t.Errorf("idempotency key = %q", request.Header.Get("Idempotency-Key"))
}
if err := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 16<<10)).Decode(&received); err != nil {
t.Errorf("decode payload: %v", err)
}
writer.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
sender, err := NewWebhookSender(WebhookConfig{Endpoint: server.URL, BearerToken: "runtime-secret", Timeout: time.Second, AllowHTTP: true}, nil)
if err != nil {
t.Fatal(err)
}
if err := sender.Send(context.Background(), validDelivery()); err != nil {
t.Fatal(err)
}
if received.Version != "1" || received.DeliveryID != "delivery-1" || received.Attempt != 1 || received.EventType != EventFiring {
t.Fatalf("payload = %#v", received)
}
}
func TestWebhookSenderRetriesWithStableIdempotencyKey(t *testing.T) {
var mu sync.Mutex
calls := 0
unique := map[string]struct{}{}
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
mu.Lock()
defer mu.Unlock()
calls++
unique[request.Header.Get("Idempotency-Key")] = struct{}{}
if calls == 1 {
writer.WriteHeader(http.StatusServiceUnavailable)
_, _ = writer.Write([]byte("token=must-not-leak"))
return
}
writer.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
sender, err := NewWebhookSender(WebhookConfig{Endpoint: server.URL, Timeout: time.Second, AllowHTTP: true}, nil)
if err != nil {
t.Fatal(err)
}
delivery := validDelivery()
if err := sender.Send(context.Background(), delivery); err == nil || strings.Contains(err.Error(), "must-not-leak") {
t.Fatalf("first error = %v", err)
}
delivery.Attempt = 2
if err := sender.Send(context.Background(), delivery); err != nil {
t.Fatal(err)
}
if calls != 2 || len(unique) != 1 {
t.Fatalf("calls=%d unique idempotency keys=%d", calls, len(unique))
}
}
func TestWebhookSenderBlocksRedirectsAndUnsafeConfiguration(t *testing.T) {
reached := false
target := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached = true }))
defer target.Close()
redirect := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
http.Redirect(writer, request, target.URL, http.StatusTemporaryRedirect)
}))
defer redirect.Close()
sender, err := NewWebhookSender(WebhookConfig{Endpoint: redirect.URL, Timeout: time.Second, AllowHTTP: true}, nil)
if err != nil {
t.Fatal(err)
}
if err := sender.Send(context.Background(), validDelivery()); err == nil || reached {
t.Fatalf("redirect result=%v reached=%v", err, reached)
}
for _, endpoint := range []string{"http://example.test", "https://user:pass@example.test", "https://example.test/hook?token=value", "https://example.test/hook#fragment"} {
if _, err := NewWebhookSender(WebhookConfig{Endpoint: endpoint, Timeout: time.Second}, nil); err == nil {
t.Fatalf("unsafe endpoint accepted: %s", endpoint)
}
}
}
func TestWebhookFactoryUsesOnlyAllowlistedSecretReference(t *testing.T) {
factory := WebhookFactory{Secrets: SecretResolverFunc(func(_ context.Context, ref SecretRef) (string, error) {
if ref.ID != WebhookSecretReference {
return "", ErrNotFound
}
return "runtime-secret", nil
}), AllowHTTP: true}
channel := Channel{Type: "webhook", SecretRef: SecretRef{ID: "env:arbitrary"}, Configuration: map[string]any{"url": "http://example.test", "timeoutSeconds": 1.0}}
if _, err := factory.Sender(context.Background(), channel); !errors.Is(err, ErrNotFound) {
t.Fatalf("unexpected error: %v", err)
}
channel.SecretRef.ID = WebhookSecretReference
if _, err := factory.Sender(context.Background(), channel); err != nil {
t.Fatal(err)
}
}
func TestWebhookSenderHonorsCanceledContext(t *testing.T) {
sender, err := NewWebhookSender(WebhookConfig{Endpoint: "https://example.test/hook", Timeout: time.Second}, nil)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := sender.Send(ctx, validDelivery()); !errors.Is(err, context.Canceled) {
t.Fatalf("error = %v", err)
}
}
func TestWebhookSenderReportsUnreachableReceiverWithoutLeakingCredential(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
endpoint := server.URL
server.Close()
sender, err := NewWebhookSender(WebhookConfig{Endpoint: endpoint, BearerToken: "unreachable-secret", Timeout: time.Second, AllowHTTP: true}, nil)
if err != nil {
t.Fatal(err)
}
err = sender.Send(context.Background(), validDelivery())
if err == nil || !strings.Contains(err.Error(), "unreachable") || strings.Contains(err.Error(), "unreachable-secret") {
t.Fatalf("error = %v", err)
}
}