mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-12 22:28:31 +00:00
Test coverage for calculateBackoff (exponential backoff calculation), NotificationQueueStatus constants, and QueuedNotification struct fields. 15 test cases covering backoff timing, cap behavior, and struct defaults.
203 lines
4.8 KiB
Go
203 lines
4.8 KiB
Go
package notifications
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCalculateBackoff(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
attempt int
|
|
expected time.Duration
|
|
}{
|
|
{
|
|
name: "attempt 0 (first retry)",
|
|
attempt: 0,
|
|
expected: 1 * time.Second,
|
|
},
|
|
{
|
|
name: "attempt 1",
|
|
attempt: 1,
|
|
expected: 2 * time.Second,
|
|
},
|
|
{
|
|
name: "attempt 2",
|
|
attempt: 2,
|
|
expected: 4 * time.Second,
|
|
},
|
|
{
|
|
name: "attempt 3",
|
|
attempt: 3,
|
|
expected: 8 * time.Second,
|
|
},
|
|
{
|
|
name: "attempt 4",
|
|
attempt: 4,
|
|
expected: 16 * time.Second,
|
|
},
|
|
{
|
|
name: "attempt 5",
|
|
attempt: 5,
|
|
expected: 32 * time.Second,
|
|
},
|
|
{
|
|
name: "attempt 6 (capped at 60s)",
|
|
attempt: 6,
|
|
expected: 60 * time.Second,
|
|
},
|
|
{
|
|
name: "attempt 7 (stays at cap)",
|
|
attempt: 7,
|
|
expected: 60 * time.Second,
|
|
},
|
|
{
|
|
name: "attempt 10 (stays at cap)",
|
|
attempt: 10,
|
|
expected: 60 * time.Second,
|
|
},
|
|
// Note: For very large attempt numbers (>= 60 on 64-bit), bit shift
|
|
// overflows causing duration to be 0. In practice this never happens
|
|
// as max_attempts is typically 3-10.
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := calculateBackoff(tc.attempt)
|
|
if result != tc.expected {
|
|
t.Errorf("calculateBackoff(%d) = %v, want %v", tc.attempt, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCalculateBackoff_ExponentialGrowth(t *testing.T) {
|
|
// Verify backoff grows exponentially until cap
|
|
prev := calculateBackoff(0)
|
|
for attempt := 1; attempt <= 5; attempt++ {
|
|
curr := calculateBackoff(attempt)
|
|
if curr != prev*2 {
|
|
t.Errorf("calculateBackoff(%d) = %v, expected %v (2x previous)", attempt, curr, prev*2)
|
|
}
|
|
prev = curr
|
|
}
|
|
}
|
|
|
|
func TestCalculateBackoff_NeverExceedsCap(t *testing.T) {
|
|
cap := 60 * time.Second
|
|
// Test a range of practical attempt values (0-20 is realistic range)
|
|
for attempt := 0; attempt <= 20; attempt++ {
|
|
result := calculateBackoff(attempt)
|
|
if result > cap {
|
|
t.Errorf("calculateBackoff(%d) = %v, exceeds cap of %v", attempt, result, cap)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNotificationQueueStatus_Values(t *testing.T) {
|
|
// Verify status constants have expected string values
|
|
tests := []struct {
|
|
status NotificationQueueStatus
|
|
expected string
|
|
}{
|
|
{QueueStatusPending, "pending"},
|
|
{QueueStatusSending, "sending"},
|
|
{QueueStatusSent, "sent"},
|
|
{QueueStatusFailed, "failed"},
|
|
{QueueStatusDLQ, "dlq"},
|
|
{QueueStatusCancelled, "cancelled"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(string(tc.status), func(t *testing.T) {
|
|
if string(tc.status) != tc.expected {
|
|
t.Errorf("status = %q, want %q", tc.status, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueuedNotification_Fields(t *testing.T) {
|
|
now := time.Now()
|
|
lastAttempt := now.Add(-1 * time.Minute)
|
|
nextRetry := now.Add(5 * time.Minute)
|
|
errorMsg := "connection refused"
|
|
|
|
notif := QueuedNotification{
|
|
ID: "test-123",
|
|
Type: "email",
|
|
Method: "smtp",
|
|
Status: QueueStatusPending,
|
|
Alerts: nil,
|
|
Config: []byte(`{"host":"smtp.example.com"}`),
|
|
Attempts: 2,
|
|
MaxAttempts: 5,
|
|
LastAttempt: &lastAttempt,
|
|
LastError: &errorMsg,
|
|
CreatedAt: now,
|
|
NextRetryAt: &nextRetry,
|
|
}
|
|
|
|
if notif.ID != "test-123" {
|
|
t.Errorf("ID = %q, want 'test-123'", notif.ID)
|
|
}
|
|
if notif.Type != "email" {
|
|
t.Errorf("Type = %q, want 'email'", notif.Type)
|
|
}
|
|
if notif.Method != "smtp" {
|
|
t.Errorf("Method = %q, want 'smtp'", notif.Method)
|
|
}
|
|
if notif.Status != QueueStatusPending {
|
|
t.Errorf("Status = %q, want 'pending'", notif.Status)
|
|
}
|
|
if notif.Attempts != 2 {
|
|
t.Errorf("Attempts = %d, want 2", notif.Attempts)
|
|
}
|
|
if notif.MaxAttempts != 5 {
|
|
t.Errorf("MaxAttempts = %d, want 5", notif.MaxAttempts)
|
|
}
|
|
if notif.LastAttempt == nil {
|
|
t.Error("LastAttempt should not be nil")
|
|
}
|
|
if notif.LastError == nil || *notif.LastError != "connection refused" {
|
|
t.Errorf("LastError = %v, want 'connection refused'", notif.LastError)
|
|
}
|
|
if notif.NextRetryAt == nil {
|
|
t.Error("NextRetryAt should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestQueuedNotification_ZeroValues(t *testing.T) {
|
|
notif := QueuedNotification{}
|
|
|
|
if notif.ID != "" {
|
|
t.Error("ID should be empty by default")
|
|
}
|
|
if notif.Type != "" {
|
|
t.Error("Type should be empty by default")
|
|
}
|
|
if notif.Status != "" {
|
|
t.Error("Status should be empty by default")
|
|
}
|
|
if notif.Attempts != 0 {
|
|
t.Error("Attempts should be 0 by default")
|
|
}
|
|
if notif.MaxAttempts != 0 {
|
|
t.Error("MaxAttempts should be 0 by default")
|
|
}
|
|
if notif.LastAttempt != nil {
|
|
t.Error("LastAttempt should be nil by default")
|
|
}
|
|
if notif.LastError != nil {
|
|
t.Error("LastError should be nil by default")
|
|
}
|
|
if !notif.CreatedAt.IsZero() {
|
|
t.Error("CreatedAt should be zero by default")
|
|
}
|
|
if notif.NextRetryAt != nil {
|
|
t.Error("NextRetryAt should be nil by default")
|
|
}
|
|
if notif.CompletedAt != nil {
|
|
t.Error("CompletedAt should be nil by default")
|
|
}
|
|
}
|