Files
rmm-openwrt/server/internal/store/notifications_test.go
T

168 lines
6.9 KiB
Go

package store
import (
"context"
"database/sql"
"path/filepath"
"testing"
"time"
"rmm-openwrt/server/internal/model"
)
func TestMigrateLegacyNotificationDeliveries(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "legacy-notifications.db")
db, err := sql.Open("sqlite", path)
if err != nil {
t.Fatal(err)
}
_, err = db.Exec(`
CREATE TABLE notification_deliveries (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL, device_id TEXT, alert_id TEXT NOT NULL DEFAULT '',
dedupe_key TEXT NOT NULL UNIQUE, event TEXT NOT NULL, channel TEXT NOT NULL, status TEXT NOT NULL,
title TEXT NOT NULL, body TEXT NOT NULL, destination TEXT NOT NULL, destination_masked TEXT NOT NULL,
error TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, sent_at TEXT, updated_at TEXT NOT NULL
);
INSERT INTO notification_deliveries VALUES
('ntf_sent', 'usr_legacy', NULL, '', 'sent', 'test', 'email', 'sent', 'sent', 'body', 'a@example.test', 'a***@example.test', '', '2026-01-01T00:00:00Z', '2026-01-01T00:00:01Z', '2026-01-01T00:00:01Z'),
('ntf_failed', 'usr_legacy', NULL, '', 'failed', 'test', 'email', 'failed', 'failed', 'body', 'a@example.test', 'a***@example.test', 'old error', '2026-01-01T00:00:00Z', NULL, '2026-01-01T00:00:01Z'),
('ntf_queued', 'usr_legacy', NULL, '', 'queued', 'test', 'email', 'queued', 'queued', 'body', 'a@example.test', 'a***@example.test', '', '2026-01-01T00:00:00Z', NULL, '2026-01-01T00:00:01Z');
`)
if err != nil {
db.Close()
t.Fatal(err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
st, err := OpenSQLite(ctx, path)
if err != nil {
t.Fatal(err)
}
defer st.Close()
history, err := st.ListNotificationDeliveries(ctx, NotificationListOptions{UserID: "usr_legacy"})
if err != nil || len(history) != 3 {
t.Fatalf("migrated history: %#v err=%v", history, err)
}
byID := make(map[string]model.NotificationDelivery, len(history))
for _, delivery := range history {
byID[delivery.ID] = delivery
}
if byID["ntf_sent"].NextAttemptAt != nil {
t.Fatalf("sent delivery became retryable: %#v", byID["ntf_sent"])
}
if byID["ntf_failed"].Status != "dead_letter" || byID["ntf_failed"].AttemptCount != 1 {
t.Fatalf("failed delivery was not migrated safely: %#v", byID["ntf_failed"])
}
if byID["ntf_queued"].NextAttemptAt == nil {
t.Fatalf("queued delivery did not become claimable: %#v", byID["ntf_queued"])
}
}
func TestNotificationQueueSurvivesRestartAndRetries(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "notification-queue.db")
st, err := OpenSQLite(ctx, path)
if err != nil {
t.Fatal(err)
}
user, err := st.EnsureBootstrapUser(ctx, "admin", "test-password-hash")
if err != nil {
t.Fatal(err)
}
created, inserted, err := st.CreateNotificationDelivery(ctx, model.NotificationDelivery{
UserID: user.ID, Event: "test", Channel: "email", Title: "test", Body: "body",
Destination: "owner@example.test", DestinationMasked: "o***@example.test", MaxAttempts: 3,
}, "restart-retry")
if err != nil || !inserted {
t.Fatalf("create delivery: inserted=%v err=%v", inserted, err)
}
if err := st.Close(); err != nil {
t.Fatal(err)
}
st, err = OpenSQLite(ctx, path)
if err != nil {
t.Fatal(err)
}
defer st.Close()
now := time.Now().UTC().Add(time.Second)
claimed, found, err := st.ClaimNotificationDelivery(ctx, created.ID, now, time.Minute)
if err != nil || !found {
t.Fatalf("claim after restart: found=%v err=%v", found, err)
}
if claimed.Status != "sending" || claimed.AttemptCount != 1 || claimed.MaxAttempts != 3 {
t.Fatalf("unexpected first claim: %#v", claimed)
}
next := now.Add(30 * time.Second)
if err := st.CompleteNotificationDelivery(ctx, claimed.ID, "retry", "temporary failure", &next); err != nil {
t.Fatal(err)
}
if deliveries, err := st.ClaimNotificationDeliveries(ctx, next.Add(-time.Second), time.Minute, 10); err != nil || len(deliveries) != 0 {
t.Fatalf("delivery became ready too early: deliveries=%#v err=%v", deliveries, err)
}
claimed, found, err = st.ClaimNotificationDelivery(ctx, created.ID, next, time.Minute)
if err != nil || !found || claimed.AttemptCount != 2 {
t.Fatalf("claim retry: delivery=%#v found=%v err=%v", claimed, found, err)
}
if err := st.CompleteNotificationDelivery(ctx, claimed.ID, "sent", "", nil); err != nil {
t.Fatal(err)
}
history, err := st.ListNotificationDeliveries(ctx, NotificationListOptions{UserID: user.ID})
if err != nil || len(history) != 1 {
t.Fatalf("list history: %#v err=%v", history, err)
}
if history[0].Status != "sent" || history[0].AttemptCount != 2 || history[0].SentAt == nil {
t.Fatalf("unexpected completed delivery: %#v", history[0])
}
}
func TestNotificationQueueDeadLetterLeaseRecoveryAndRetention(t *testing.T) {
ctx := context.Background()
st, err := OpenSQLite(ctx, filepath.Join(t.TempDir(), "notification-dead-letter.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
user, err := st.EnsureBootstrapUser(ctx, "admin", "test-password-hash")
if err != nil {
t.Fatal(err)
}
created, inserted, err := st.CreateNotificationDelivery(ctx, model.NotificationDelivery{
UserID: user.ID, Event: "active", Channel: "telegram", Title: "test", Body: "body",
Destination: "12345", DestinationMasked: "***2345", MaxAttempts: 1,
}, "lease-dead-letter")
if err != nil || !inserted {
t.Fatalf("create delivery: inserted=%v err=%v", inserted, err)
}
now := time.Now().UTC().Add(time.Second)
claimed, found, err := st.ClaimNotificationDelivery(ctx, created.ID, now, time.Minute)
if err != nil || !found || claimed.AttemptCount != 1 {
t.Fatalf("initial claim: delivery=%#v found=%v err=%v", claimed, found, err)
}
if deliveries, err := st.ClaimNotificationDeliveries(ctx, now.Add(2*time.Minute), time.Minute, 10); err != nil || len(deliveries) != 0 {
t.Fatalf("exhausted lease was reclaimed: deliveries=%#v err=%v", deliveries, err)
}
history, err := st.ListNotificationDeliveries(ctx, NotificationListOptions{UserID: user.ID})
if err != nil || len(history) != 1 || history[0].Status != "dead_letter" {
t.Fatalf("delivery was not moved to dead letter: %#v err=%v", history, err)
}
queued, inserted, err := st.CreateNotificationDelivery(ctx, model.NotificationDelivery{
UserID: user.ID, Event: "test", Channel: "email", Title: "queued", Body: "body",
Destination: "owner@example.test", DestinationMasked: "o***@example.test",
}, "retention-keeps-queued")
if err != nil || !inserted || queued.Status != "queued" {
t.Fatalf("create queued delivery: %#v inserted=%v err=%v", queued, inserted, err)
}
deleted, err := st.PurgeNotificationDeliveriesBefore(ctx, time.Now().UTC().Add(24*time.Hour))
if err != nil || deleted != 1 {
t.Fatalf("purge terminal history: deleted=%d err=%v", deleted, err)
}
history, err = st.ListNotificationDeliveries(ctx, NotificationListOptions{UserID: user.ID})
if err != nil || len(history) != 1 || history[0].ID != queued.ID {
t.Fatalf("purge removed non-terminal delivery: %#v err=%v", history, err)
}
}