Public source validation / validate (push) Failing after 3m8s
145 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|