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) } }