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