Files
tiennm99bot/internal/modules/util/subscription/subscription_test.go
T
tiennm99 59c74de9a9 refactor(lol): move the subscriber list and daily push fan-out into a shared package
The subscriber store, the once-per-day claim, the terminal-error
classifier and the throttled fan-out with dead-chat pruning now live in
internal/modules/util/subscription so another module can offer an
opt-in daily push. Stored document shapes and keys are unchanged.
2026-10-02 13:06:07 +07:00

168 lines
4.3 KiB
Go

package subscription
import (
"context"
"testing"
"github.com/tiennm99/miti99bot/internal/storage"
)
func newStore() Store {
return storage.Typed[Doc](storage.NewMemoryProvider().Collection("test"))
}
func TestSubscribers_AddRemoveListIdempotent(t *testing.T) {
ctx := context.Background()
store := newStore()
got, _ := List(ctx, store)
if len(got) != 0 {
t.Errorf("empty list = %v, want []", got)
}
added, err := Add(ctx, store, 42, 0)
if err != nil || !added {
t.Fatalf("first add: added=%v err=%v", added, err)
}
added, _ = Add(ctx, store, 42, 0)
if added {
t.Errorf("re-add should be no-op")
}
got, _ = List(ctx, store)
if len(got) != 1 || got[0] != (Subscriber{ChatID: 42}) {
t.Errorf("after add(42,0): %v, want [{42 0}]", got)
}
if _, err := Add(ctx, store, 7, 0); err != nil {
t.Fatal(err)
}
got, _ = List(ctx, store)
if len(got) != 2 {
t.Errorf("after add(7,0): %v, want len 2", got)
}
removed, err := Remove(ctx, store, 42, 0)
if err != nil || !removed {
t.Fatalf("remove(42,0): removed=%v err=%v", removed, err)
}
removed, _ = Remove(ctx, store, 42, 0)
if removed {
t.Errorf("re-remove should be no-op")
}
got, _ = List(ctx, store)
if len(got) != 1 || got[0].ChatID != 7 {
t.Errorf("after remove(42,0): %v, want [{7 0}]", got)
}
}
// TestSubscribers_TopicsAreDistinct locks in the forum-topic fix: the same
// chat can subscribe in multiple topics independently, and removing one
// topic must not affect the others.
func TestSubscribers_TopicsAreDistinct(t *testing.T) {
ctx := context.Background()
store := newStore()
for _, tid := range []int{0, 5, 9} {
added, err := Add(ctx, store, 100, tid)
if err != nil || !added {
t.Fatalf("add(100,%d): added=%v err=%v", tid, added, err)
}
}
// Same (chat, thread) is rejected as duplicate.
if added, _ := Add(ctx, store, 100, 5); added {
t.Errorf("duplicate (100,5) should be no-op")
}
got, _ := List(ctx, store)
if len(got) != 3 {
t.Fatalf("after 3 adds: %v, want len 3", got)
}
// Removing one topic leaves the other two intact.
if removed, _ := Remove(ctx, store, 100, 5); !removed {
t.Error("remove(100,5) should report removed=true")
}
got, _ = List(ctx, store)
if len(got) != 2 {
t.Errorf("after remove(100,5): %v, want len 2", got)
}
for _, s := range got {
if s.ChatID == 100 && s.ThreadID == 5 {
t.Errorf("(100,5) still present: %v", got)
}
}
}
// TestSubscribers_RemoveAllForChat covers the chat-wide prune used when a
// terminal error means every topic in that chat is dead too.
func TestSubscribers_RemoveAllForChat(t *testing.T) {
ctx := context.Background()
store := newStore()
for _, tid := range []int{0, 5, 9} {
if _, err := Add(ctx, store, 100, tid); err != nil {
t.Fatal(err)
}
}
if _, err := Add(ctx, store, 200, 0); err != nil {
t.Fatal(err)
}
n, err := RemoveAllForChat(ctx, store, 100)
if err != nil {
t.Fatalf("RemoveAllForChat(100): %v", err)
}
if n != 3 {
t.Errorf("RemoveAllForChat(100) returned %d, want 3", n)
}
got, _ := List(ctx, store)
if len(got) != 1 || got[0].ChatID != 200 {
t.Errorf("after wipe(100): %v, want only chat 200", got)
}
// Idempotent: second call removes nothing.
if n, _ := RemoveAllForChat(ctx, store, 100); n != 0 {
t.Errorf("second RemoveAllForChat(100) = %d, want 0", n)
}
}
func TestSubscribers_CurrentShapeRoundTrip(t *testing.T) {
ctx := context.Background()
store := newStore()
// Write the current shape directly via Put so we can confirm round-trip.
currentSubs := []Subscriber{{ChatID: 11}, {ChatID: 22}, {ChatID: 33}}
if err := store.Put(ctx, Key, Doc{Subscribers: currentSubs}); err != nil {
t.Fatal(err)
}
got, err := List(ctx, store)
if err != nil {
t.Fatalf("listSubscribers: %v", err)
}
want := []Subscriber{{ChatID: 11}, {ChatID: 22}, {ChatID: 33}}
if len(got) != len(want) {
t.Fatalf("decode: got %v, want %v", got, want)
}
for i, s := range got {
if s != want[i] {
t.Errorf("[%d]: got %v, want %v", i, s, want[i])
}
}
// Next mutation verifies subscribers wrap correctly.
if _, err := Add(ctx, store, 44, 7); err != nil {
t.Fatal(err)
}
doc, _, _ := store.Get(ctx, Key)
if doc.Subscribers == nil {
t.Error("expected non-nil Subscribers field after add")
}
if len(doc.Subscribers) != 4 {
t.Errorf("after add: doc.Subscribers len = %d, want 4", len(doc.Subscribers))
}
}