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.
182 lines
5.9 KiB
182 lines
5.9 KiB
|
2 months ago
|
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{}
|
||
|
|
}
|