package store import ( "context" "strings" "testing" "time" ) // TestNotificationDeliveryLog covers the Phase 7 delivery row: a delivered // brief records the channel, the tag and sent_at, and a failure records the // error without a sent_at, so a failure can never read as delivered (FR-12). func TestNotificationDeliveryLog(t *testing.T) { ctx := context.Background() st := newTestStore(t) sent := time.Date(2026, 10, 16, 6, 0, 0, 0, time.UTC) okID, err := st.RecordNotification(ctx, Notification{ Scope: "Harry", Recipient: "Harry", Channel: "email", Tag: "harry_email", Text: "Harry — 1 overdue.", OK: true, SentAt: &sent, }) if err != nil { t.Fatalf("record ok: %v", err) } if _, err := st.RecordNotification(ctx, Notification{ Scope: "Harry", Recipient: "Harry", Channel: "whatsapp", Tag: "harry_whatsapp", Text: "Harry — 1 overdue.", OK: false, Error: "apprise returned status 424", }); err != nil { t.Fatalf("record fail: %v", err) } got, err := st.NotificationByID(ctx, okID) if err != nil || got == nil { t.Fatalf("by id: %v %v", got, err) } if !got.OK || got.Channel != "email" || got.Tag != "harry_email" || got.SentAt == nil { t.Errorf("row = %+v", got) } rows, err := st.RecentNotifications(ctx, 10) if err != nil || len(rows) != 2 { t.Fatalf("recent = %d, %v", len(rows), err) } var failed *Notification for i := range rows { if !rows[i].OK { failed = &rows[i] } } if failed == nil { t.Fatal("the failed delivery row is missing") } if failed.SentAt != nil { t.Error("a failed delivery must not carry sent_at") } if failed.Error == "" { t.Error("a failed delivery must record its error") } st2, err := st.NotificationSummary(ctx) if err != nil { t.Fatalf("summary: %v", err) } if st2.Total != 2 || st2.Sent != 1 || st2.Failed != 1 || st2.Drafts != 0 { t.Errorf("summary = %+v", st2) } if st2.Last == nil || st2.Last.Tag != "harry_whatsapp" { t.Errorf("last = %+v", st2.Last) } } // TestDraftAndMarkSent covers the Send action on a stored brief (FR-10): a // draft starts unsent and is stamped only when a channel delivered. func TestDraftAndMarkSent(t *testing.T) { ctx := context.Background() st := newTestStore(t) id, err := st.CreateDraft(ctx, "combined", "Jo+Sam", "School brief — Harry and Finn") if err != nil { t.Fatalf("draft: %v", err) } draft, err := st.NotificationByID(ctx, id) if err != nil || draft == nil { t.Fatalf("by id: %v %v", draft, err) } if draft.Scope != "combined" || draft.Channel != "" || draft.OK || draft.SentAt != nil { t.Errorf("draft = %+v", draft) } at := time.Date(2026, 10, 16, 6, 1, 0, 0, time.UTC) if err := st.MarkSummarySent(ctx, id, "email", "joanna_email", true, "", at); err != nil { t.Fatalf("mark sent: %v", err) } got, _ := st.NotificationByID(ctx, id) if got == nil || !got.OK || got.SentAt == nil || got.Channel != "email" { t.Errorf("after send = %+v", got) } st2, _ := st.NotificationSummary(ctx) if st2.Drafts != 0 || st2.Sent != 1 { t.Errorf("summary = %+v", st2) } } // TestNotificationSummaryCountsFailures proves a failed send is visible in the // summary the dashboard shows (FR-12). func TestNotificationSummaryCountsFailures(t *testing.T) { ctx := context.Background() st := newTestStore(t) if _, err := st.RecordNotification(ctx, Notification{ Scope: "combined", Recipient: "Jo", Channel: "whatsapp", Tag: "joanna_whatsapp", Text: "brief", Error: "apprise returned status 424: ", }); err != nil { t.Fatalf("record: %v", err) } status, err := st.NotificationSummary(ctx) if err != nil { t.Fatalf("summary: %v", err) } if status.Failed != 1 || status.Sent != 0 || !status.EverRun() { t.Errorf("status = %+v", status) } if !strings.Contains(status.Last.Error, "424") { t.Errorf("last error = %q", status.Last.Error) } }