Files
aiaa-notification-server/internal/safew/watcher_test.go
T
ryan e369f398d8 feat: enhance Safew error handling and polling mechanism
- Added error_msg field to safewAPIResponse for better error descriptions.
- Updated safewErrorDescription function to include error_msg in the output.
- Introduced a new test to verify that error messages are included in Safew error descriptions.
- Improved the polling mechanism in the Watcher to handle conflicts and ensure no overlapping polls occur.
2026-08-15 01:07:01 +08:00

124 lines
3.2 KiB
Go

package safew
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
"aiaa-notification-service/internal/adapter"
)
func TestRefreshMergesAndAdvancesOffset(t *testing.T) {
st := NewMemStore()
poll := func(token string, offset int64, timeout int) ([]adapter.SafewChat, int64, error) {
if timeout != 0 {
return nil, offset, nil
}
if offset != 0 {
t.Fatalf("offset=%d", offset)
}
return []adapter.SafewChat{{ID: "10000778141", Type: "group", Title: "测试AI"}}, 11, nil
}
w := NewWatcher(st, poll)
ctx := context.Background()
if err := w.Refresh(ctx, "tok"); err != nil {
t.Fatal(err)
}
list, err := w.List(ctx, "tok", "测试")
if err != nil {
t.Fatal(err)
}
if len(list) != 1 || list[0].ID != "10000778141" {
t.Fatalf("%#v", list)
}
off, _ := st.GetSafewOffset(ctx, "tok")
if off != 11 {
t.Fatalf("offset=%d", off)
}
w.Stop()
}
func TestRefreshAuthError(t *testing.T) {
st := NewMemStore()
w := NewWatcher(st, func(string, int64, int) ([]adapter.SafewChat, int64, error) {
return nil, 0, &adapter.SafewAuthError{Description: "BOT_TOKEN_INVALID"}
})
err := w.Refresh(context.Background(), "bad")
if err == nil {
t.Fatal("expected auth error")
}
if _, ok := err.(*adapter.SafewAuthError); !ok {
t.Fatalf("%T", err)
}
w.Stop()
}
type alwaysLockStore struct{ *MemStore }
func (a *alwaysLockStore) TrySafewPollLock(context.Context, string, time.Duration) (bool, error) {
return true, nil
}
func (a *alwaysLockStore) UnlockSafewPoll(context.Context, string) error { return nil }
func TestRefreshAndEnsureDoNotOverlapPolls(t *testing.T) {
st := &alwaysLockStore{MemStore: NewMemStore()}
var inflight int32
var overlap int32
poll := func(token string, offset int64, timeout int) ([]adapter.SafewChat, int64, error) {
n := atomic.AddInt32(&inflight, 1)
if n > 1 {
atomic.AddInt32(&overlap, 1)
}
time.Sleep(40 * time.Millisecond)
atomic.AddInt32(&inflight, -1)
return []adapter.SafewChat{{ID: "1", Type: "group", Title: "g"}}, offset + 1, nil
}
w := NewWatcher(st, poll)
w.bgIdle = 20 * time.Millisecond
ctx := context.Background()
errCh := make(chan error, 1)
go func() { errCh <- w.Refresh(ctx, "tok") }()
w.Ensure("tok")
if err := <-errCh; err != nil {
t.Fatal(err)
}
time.Sleep(80 * time.Millisecond)
w.Stop()
if atomic.LoadInt32(&overlap) > 0 {
t.Fatalf("overlapping getUpdates: %d", overlap)
}
}
func TestEnsurePollsInBackground(t *testing.T) {
st := NewMemStore()
got := make(chan int, 1)
w := NewWatcher(st, func(token string, offset int64, timeout int) ([]adapter.SafewChat, int64, error) {
if timeout != 30 {
return nil, offset, nil
}
select {
case got <- timeout:
default:
}
return []adapter.SafewChat{{ID: "1", Type: "group", Title: "g"}}, offset + 1, nil
})
w.bgIdle = 10 * time.Millisecond
w.Ensure("tok")
select {
case <-got:
case <-time.After(2 * time.Second):
t.Fatal("background poll not called")
}
w.Stop()
}
func TestPollConflictIsRetryable(t *testing.T) {
err := errors.New("safew getUpdates status 400: BAD_REQUEST: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running")
if !isGetUpdatesConflict(err) {
t.Fatal("expected conflict")
}
}