You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
6.7 KiB

package alarmdelivery
import (
"context"
"errors"
"net"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
)
func TestDispatcherPersistsDeliveredStatus(t *testing.T) {
statePath := filepath.Join(t.TempDir(), "deliveries.json")
dispatcher, err := NewDispatcher(map[string]Transport{ChannelWeb: WebTransport{}}, DispatcherOptions{
StatePath: statePath, PollInterval: 10 * time.Millisecond,
})
if err != nil {
t.Fatal(err)
}
result, err := dispatcher.Deliver(context.Background(), validRequest(ChannelWeb))
if err != nil {
t.Fatal(err)
}
completed := waitForDeliveryState(t, dispatcher, result.DeliveryID, "delivered", 2*time.Second)
if completed.Attempts != 1 || completed.CompletedAt == nil {
t.Fatalf("completed delivery = %+v", completed)
}
dispatcher.Close()
reloaded, err := NewDispatcher(map[string]Transport{ChannelWeb: WebTransport{}}, DispatcherOptions{
StatePath: statePath, PollInterval: 10 * time.Millisecond,
})
if err != nil {
t.Fatal(err)
}
defer reloaded.Close()
persisted, err := reloaded.Status(context.Background(), result.DeliveryID)
if err != nil || persisted.State != "delivered" || persisted.Attempts != 1 {
t.Fatalf("persisted delivery = %+v, %v", persisted, err)
}
}
func TestDispatcherRetriesThenDelivers(t *testing.T) {
var attempts atomic.Int32
transport := TransportFunc(func(context.Context, Attempt) error {
if attempts.Add(1) == 1 {
return errors.New("temporary failure")
}
return nil
})
dispatcher, err := NewDispatcher(map[string]Transport{ChannelWeb: transport}, DispatcherOptions{
StatePath: filepath.Join(t.TempDir(), "deliveries.json"), PollInterval: 10 * time.Millisecond,
})
if err != nil {
t.Fatal(err)
}
defer dispatcher.Close()
request := validRequest(ChannelWeb)
request.RetryCount = 1
result, err := dispatcher.Deliver(context.Background(), request)
if err != nil {
t.Fatal(err)
}
completed := waitForDeliveryState(t, dispatcher, result.DeliveryID, "delivered", 3*time.Second)
if completed.Attempts != 2 || attempts.Load() != 2 {
t.Fatalf("retried delivery = %+v, transport attempts=%d", completed, attempts.Load())
}
}
func TestDispatcherRecordsUnconfiguredTransportFailure(t *testing.T) {
dispatcher, err := NewDispatcher(map[string]Transport{ChannelWeb: WebTransport{}}, DispatcherOptions{
StatePath: filepath.Join(t.TempDir(), "deliveries.json"), PollInterval: 10 * time.Millisecond,
})
if err != nil {
t.Fatal(err)
}
defer dispatcher.Close()
request := validRequest(ChannelEmail)
request.Email = &EmailTarget{Host: "smtp.example.local", Port: 25, From: "tera@example.local", To: []string{"ops@example.local"}}
request.RetryCount = 0
result, err := dispatcher.Deliver(context.Background(), request)
if err != nil {
t.Fatal(err)
}
failed := waitForDeliveryState(t, dispatcher, result.DeliveryID, "failed", 2*time.Second)
if failed.Attempts != 1 || !strings.Contains(failed.LastError, "email transport is not configured") {
t.Fatalf("failed delivery = %+v", failed)
}
}
func TestDispatcherQueueIsBounded(t *testing.T) {
release := make(chan struct{})
transport := TransportFunc(func(ctx context.Context, _ Attempt) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-release:
return nil
}
})
dispatcher, err := NewDispatcher(map[string]Transport{ChannelWeb: transport}, DispatcherOptions{
StatePath: filepath.Join(t.TempDir(), "deliveries.json"), MaxJobs: 2, PollInterval: 10 * time.Millisecond,
})
if err != nil {
t.Fatal(err)
}
defer dispatcher.Close()
request := validRequest(ChannelWeb)
request.QueueLimit = 1
if _, err := dispatcher.Deliver(context.Background(), request); err != nil {
t.Fatal(err)
}
if _, err := dispatcher.Deliver(context.Background(), request); err == nil || !strings.Contains(err.Error(), "queue is full") {
t.Fatalf("second delivery error = %v", err)
}
close(release)
}
func TestDispatcherRefusesNonRegularStatePath(t *testing.T) {
statePath := filepath.Join(t.TempDir(), "deliveries.json")
if err := os.Mkdir(statePath, 0o700); err != nil {
t.Fatal(err)
}
if _, err := NewDispatcher(map[string]Transport{ChannelWeb: WebTransport{}}, DispatcherOptions{StatePath: statePath}); err == nil {
t.Fatal("non-regular alarm delivery state path accepted")
}
}
func TestSyslogTransportWritesBoundedRFC5424Message(t *testing.T) {
listener, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
address := listener.LocalAddr().(*net.UDPAddr)
request := validRequest(ChannelSyslog)
request.RuleID = "auth_failures"
request.Message = "line one\nline two"
request.Syslog = &SyslogTarget{Host: "127.0.0.1", Port: address.Port, Protocol: "udp", Facility: "local0"}
if err := (SyslogTransport{}).Send(context.Background(), Attempt{DeliveryID: "alarm_delivery_20260723_000001", Channel: ChannelSyslog, Request: request}); err != nil {
t.Fatal(err)
}
_ = listener.SetReadDeadline(time.Now().Add(time.Second))
buffer := make([]byte, 2048)
count, _, err := listener.ReadFrom(buffer)
if err != nil {
t.Fatal(err)
}
payload := string(buffer[:count])
if !strings.HasPrefix(payload, "<132>1 ") || !strings.Contains(payload, "auth_failures") || strings.Contains(payload, "line one\nline two") {
t.Fatalf("syslog payload = %q", payload)
}
}
func validRequest(channel string) Request {
return Request{
Message: "operator verification", Channels: []string{channel},
RetryCount: 0, RetryIntervalSeconds: 1, QueueLimit: 10,
}
}
func waitForDeliveryState(t *testing.T, dispatcher *Dispatcher, deliveryID, state string, timeout time.Duration) Result {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
result, err := dispatcher.Status(context.Background(), deliveryID)
if err != nil {
t.Fatal(err)
}
if result.State == state {
return result
}
time.Sleep(10 * time.Millisecond)
}
result, _ := dispatcher.Status(context.Background(), deliveryID)
t.Fatalf("delivery state = %q, want %q", result.State, state)
return Result{}
}
func TestFactoryResetErasesDeliveryHistoryAndRetryState(t *testing.T) {
path := filepath.Join(t.TempDir(), "deliveries.json")
d, err := NewDispatcher(map[string]Transport{ChannelWeb: WebTransport{}}, DispatcherOptions{StatePath: path})
if err != nil {
t.Fatal(err)
}
defer d.Close()
result, err := d.Deliver(context.Background(), validRequest(ChannelWeb))
if err != nil {
t.Fatal(err)
}
if err := d.FactoryReset(context.Background()); err != nil {
t.Fatal(err)
}
if _, err := d.Status(context.Background(), result.DeliveryID); err == nil {
t.Fatal("old delivery survived")
}
reopened, err := NewDispatcher(nil, DispatcherOptions{StatePath: path})
if err != nil {
t.Fatal(err)
}
defer reopened.Close()
if len(reopened.jobs) != 0 {
t.Fatal("retry data resurrected after restart")
}
}