96 lines
3.5 KiB
Go
96 lines
3.5 KiB
Go
package alertnotification
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"data-terminal-cloud/internal/outbox"
|
|
)
|
|
|
|
func TestProcessorDeliversSignedEnvelope(t *testing.T) {
|
|
secret := "test-alert-webhook-secret-at-least-32-bytes"
|
|
now := time.Unix(1_800_000_000, 0).UTC()
|
|
var received bool
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
received = true
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.Header.Get("X-Voice-Event-ID") != "10000000-0000-0000-0000-000000000001" || r.Header.Get("X-Voice-Timestamp") != "1800000000" {
|
|
t.Fatalf("missing delivery identity headers")
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
_, _ = mac.Write([]byte("1800000000."))
|
|
_, _ = mac.Write(body)
|
|
want := "v1=" + hex.EncodeToString(mac.Sum(nil))
|
|
if r.Header.Get("X-Voice-Signature") != want {
|
|
t.Fatalf("signature=%q want=%q", r.Header.Get("X-Voice-Signature"), want)
|
|
}
|
|
var envelope webhookEnvelope
|
|
if err = json.Unmarshal(body, &envelope); err != nil || envelope.EventType != "alert.webhook_requested" || string(envelope.Data) != `{"severity":"HIGH"}` {
|
|
t.Fatalf("envelope=%+v err=%v", envelope, err)
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
processor, err := NewProcessor(server.URL, secret, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
processor.now = func() time.Time { return now }
|
|
event := outbox.Event{ID: "10000000-0000-0000-0000-000000000001", EventType: "alert.webhook_requested", Payload: json.RawMessage(`{"severity":"HIGH"}`)}
|
|
if err = processor.Handle(context.Background(), event); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !received {
|
|
t.Fatal("webhook was not delivered")
|
|
}
|
|
}
|
|
|
|
func TestProcessorRetryClassificationAndDisabledMode(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
http.Error(w, "temporarily unavailable", http.StatusServiceUnavailable)
|
|
}))
|
|
defer server.Close()
|
|
processor, err := NewProcessor(server.URL, "test-alert-webhook-secret-at-least-32-bytes", time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
event := outbox.Event{ID: "10000000-0000-0000-0000-000000000001", EventType: "alert.webhook_requested", Payload: json.RawMessage(`{}`)}
|
|
if err = processor.Handle(context.Background(), event); !outbox.IsRetryable(err) {
|
|
t.Fatalf("503 error should be retryable: %v", err)
|
|
}
|
|
disabled, err := NewProcessor("", "", time.Second)
|
|
if err != nil || disabled.Handle(context.Background(), event) != nil {
|
|
t.Fatalf("disabled processor err=%v", err)
|
|
}
|
|
if _, err = NewProcessor(server.URL, "short", time.Second); err == nil {
|
|
t.Fatal("short signing secret was accepted")
|
|
}
|
|
}
|
|
|
|
func TestProcessorRequiresConfigurationForDeliveryTest(t *testing.T) {
|
|
t.Parallel()
|
|
processor, err := NewProcessor("", "", time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testEvent := outbox.Event{ID: "10000000-0000-0000-0000-000000000001", EventType: "alert.webhook_test_requested", Payload: json.RawMessage(`{}`)}
|
|
if err = processor.Handle(context.Background(), testEvent); err == nil || outbox.IsRetryable(err) {
|
|
t.Fatalf("expected a permanent configuration error, got %v", err)
|
|
}
|
|
regularEvent := outbox.Event{ID: testEvent.ID, EventType: "alert.webhook_requested", Payload: testEvent.Payload}
|
|
if err = processor.Handle(context.Background(), regularEvent); err != nil {
|
|
t.Fatalf("regular alert delivery must remain disabled-safe: %v", err)
|
|
}
|
|
}
|