package notify import ( "testing" "time" ) func TestPutAndDrainDelivers(t *testing.T) { s := NewStore() n := s.Put("alice", "bob", map[string]any{"text": "hi"}, false, 0) if n.Trace == "" { t.Fatal("Put should assign a trace id") } if s.PendingCount("bob") != 1 { t.Fatalf("pending for bob = %d, want 1", s.PendingCount("bob")) } got := s.Drain("bob") if len(got) != 1 || got[0].Trace != n.Trace { t.Fatalf("Drain = %+v, want the queued notification", got) } if !got[0].Delivered { t.Fatal("drained notification should be marked delivered") } if s.PendingCount("bob") != 0 { t.Fatal("pending should be empty after drain") } // Status still works after delivery. st, ok := s.Status(n.Trace) if !ok || !st.Delivered { t.Fatalf("Status after drain = %+v, ok=%v", st, ok) } } func TestExpiryDropsNotification(t *testing.T) { s := NewStore() now := time.Unix(1000, 0) s.SetClock(func() time.Time { return now }) s.Put("srv", "bob", "payload", false, 5*time.Second) // Advance past expiry; the queued message must not be delivered. now = now.Add(10 * time.Second) if got := s.Drain("bob"); len(got) != 0 { t.Fatalf("expired notification was delivered: %+v", got) } if removed := s.PurgeExpired(); removed != 0 { // Drain already dropped it from byTrace; nothing left to purge. t.Fatalf("PurgeExpired removed %d, want 0 (already dropped on drain)", removed) } } func TestPurgeExpiredReclaimsUndeliveredTraces(t *testing.T) { s := NewStore() now := time.Unix(0, 0) s.SetClock(func() time.Time { return now }) // A target that never connects: its messages must still be reclaimed. s.Put("srv", "ghost", "a", false, time.Second) s.Put("srv", "ghost", "b", false, time.Second) now = now.Add(2 * time.Second) removed := s.PurgeExpired() if removed != 2 { t.Fatalf("PurgeExpired removed %d, want 2", removed) } if s.PendingCount("ghost") != 0 { t.Fatal("expired pending queue should be gone") } } func TestPerTargetCapEvictsOldest(t *testing.T) { s := NewStore() s.maxPerTarget = 3 var traces []string for i := 0; i < 5; i++ { traces = append(traces, s.Put("srv", "bob", i, false, 0).Trace) } if s.PendingCount("bob") != 3 { t.Fatalf("pending = %d, want capped at 3", s.PendingCount("bob")) } // The two oldest must have been evicted from the trace index too. if _, ok := s.Status(traces[0]); ok { t.Fatal("oldest notification should have been evicted") } if _, ok := s.Status(traces[4]); !ok { t.Fatal("newest notification should be retained") } } func TestSuitReply(t *testing.T) { s := NewStore() n := s.Put("srv", "bob", "question", true, 0) // A non-suit reply target must fail. plain := s.Put("srv", "bob", "fyi", false, 0) if _, ok := s.Reply(plain.Trace, "nope"); ok { t.Fatal("replying to a non-suit notification should fail") } updated, ok := s.Reply(n.Trace, map[string]any{"answer": 42}) if !ok || !updated.Replied { t.Fatalf("Reply = %+v ok=%v", updated, ok) } st, _ := s.Status(n.Trace) if !st.Replied { t.Fatal("status should reflect the reply") } if m, _ := st.Reply.(map[string]any); m["answer"] != 42 { t.Fatalf("stored reply = %v", st.Reply) } } func TestUnknownTraceStatus(t *testing.T) { s := NewStore() if _, ok := s.Status("does-not-exist"); ok { t.Fatal("unknown trace should not resolve") } }