package notify import ( "context" "errors" "io" "log/slog" "sync" "sync/atomic" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/require" ) type fakeRepo struct { mu sync.Mutex inserted []Message failNext bool } func (f *fakeRepo) Insert(_ context.Context, msg Message) error { f.mu.Lock() defer f.mu.Unlock() if f.failNext { f.failNext = false return errors.New("db down") } f.inserted = append(f.inserted, msg) return nil } func (f *fakeRepo) List(context.Context, uuid.UUID, bool, int) ([]Message, error) { return nil, nil } func (f *fakeRepo) CountUnread(context.Context, uuid.UUID) (int, error) { return 0, nil } func (f *fakeRepo) MarkRead(context.Context, uuid.UUID, uuid.UUID) error { return nil } func (f *fakeRepo) MarkAllRead(context.Context, uuid.UUID) error { return nil } type spyNotifier struct { name Channel count atomic.Int32 fail bool } func (s *spyNotifier) Name() Channel { return s.name } func (s *spyNotifier) Send(context.Context, Message) error { s.count.Add(1) if s.fail { return errors.New("send failed") } return nil } func newTestLogger() *slog.Logger { return slog.New(slog.NewJSONHandler(io.Discard, nil)) } func TestDispatch_PersistsThenFansOut(t *testing.T) { repo := &fakeRepo{} spy := &spyNotifier{name: ChannelDummy} d := NewDispatcher(repo, newTestLogger()) d.Register(spy) require.NoError(t, d.Dispatch(context.Background(), Message{ UserID: uuid.New(), Topic: "x", Severity: SeverityInfo, Title: "t", Body: "b", })) require.Len(t, repo.inserted, 1) require.NotEqual(t, uuid.Nil, repo.inserted[0].ID, "ID 应被 dispatcher 填充") require.Eventually(t, func() bool { return spy.count.Load() == 1 }, time.Second, 10*time.Millisecond) } func TestDispatch_PersistFailureReturnsError_DoesNotFanout(t *testing.T) { repo := &fakeRepo{failNext: true} spy := &spyNotifier{name: ChannelDummy} d := NewDispatcher(repo, newTestLogger()) d.Register(spy) err := d.Dispatch(context.Background(), Message{UserID: uuid.New(), Topic: "x"}) require.Error(t, err) time.Sleep(50 * time.Millisecond) require.Equal(t, int32(0), spy.count.Load()) } func TestDispatch_NotifierFailureDoesNotBlockOthers(t *testing.T) { repo := &fakeRepo{} bad := &spyNotifier{name: "bad", fail: true} good := &spyNotifier{name: "good"} d := NewDispatcher(repo, newTestLogger()) d.Register(bad) d.Register(good) require.NoError(t, d.Dispatch(context.Background(), Message{UserID: uuid.New()})) require.Eventually(t, func() bool { return bad.count.Load() == 1 && good.count.Load() == 1 }, time.Second, 10*time.Millisecond) }