feat: 配置列表分页与钉钉机器人分钟级排队限流
统一 sources/templates/channels/rules 列表为分页响应,避免配置增多时全量返回;按钉钉 access_token 限制每分钟发送并在超限时等待下一分钟,降低触发官方封禁风险。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDingTalkLimitKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "token only",
|
||||
url: "https://oapi.dingtalk.com/robot/send?access_token=abc123",
|
||||
want: "abc123",
|
||||
},
|
||||
{
|
||||
name: "token with other params",
|
||||
url: "https://oapi.dingtalk.com/robot/send?access_token=tok&foo=1",
|
||||
want: "tok",
|
||||
},
|
||||
{
|
||||
name: "missing token falls back to url",
|
||||
url: "https://oapi.dingtalk.com/robot/send?foo=1",
|
||||
want: "https://oapi.dingtalk.com/robot/send?foo=1",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
url: "",
|
||||
want: "unknown",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := DingTalkLimitKey(tt.url); got != tt.want {
|
||||
t.Fatalf("DingTalkLimitKey(%q) = %q, want %q", tt.url, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDingTalkLimiterAcquireWaitsForNextWindow(t *testing.T) {
|
||||
now := time.Unix(1000, 0) // fixed
|
||||
var slept time.Duration
|
||||
store := newMemoryMinuteStore()
|
||||
lim := &DingTalkLimiter{
|
||||
store: store,
|
||||
limit: 2,
|
||||
now: func() time.Time { return now },
|
||||
sleep: func(d time.Duration) {
|
||||
slept += d
|
||||
now = now.Add(d)
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
key := "tok-1"
|
||||
if err := lim.Acquire(ctx, key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := lim.Acquire(ctx, key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if slept != 0 {
|
||||
t.Fatalf("unexpected sleep before limit: %v", slept)
|
||||
}
|
||||
|
||||
// third should wait until next minute boundary (60 - 1000%60 = 20s? 1000/60=16 rem 40, next at 17*60=1020, wait 20s)
|
||||
if err := lim.Acquire(ctx, key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if slept != 20*time.Second {
|
||||
t.Fatalf("slept = %v, want 20s", slept)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDingTalkLimiterSameKeyShared(t *testing.T) {
|
||||
now := time.Unix(0, 0)
|
||||
store := newMemoryMinuteStore()
|
||||
lim := &DingTalkLimiter{
|
||||
store: store,
|
||||
limit: 1,
|
||||
now: func() time.Time { return now },
|
||||
sleep: func(d time.Duration) { now = now.Add(d) },
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := lim.Acquire(ctx, "shared"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := lim.Acquire(ctx, "shared"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if now.Unix() != 60 {
|
||||
t.Fatalf("expected wait to next minute boundary, now=%v", now)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user