feat(channel/session): 渠道↔会话映射 + 主人识别(actor元数据+白名单,不读消息内容) + 群聊隔离 + 主动推送策略(iLink24h/OneBot限频)
This commit is contained in:
1 parent
3ce9489f8e
commit
7adaf490c7
7 files changed
+1133
No files matched your search
@@ -0,0 +1,171 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
)
|
||||
|
||||
func capsProactive() adapter.Capabilities {
|
||||
return adapter.Capabilities{Proactive: true}
|
||||
}
|
||||
|
||||
func capsNoProactive() adapter.Capabilities {
|
||||
return adapter.Capabilities{Proactive: false}
|
||||
}
|
||||
|
||||
func TestProactivePolicyCapabilityGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewProactivePolicy(ProactiveOptions{})
|
||||
// A channel that does not declare proactive support is denied regardless of
|
||||
// how fresh the last message is.
|
||||
if p.Allow(context.Background(), capsNoProactive(), time.Now()) {
|
||||
t.Error("Allow() = true with Proactive=false, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProactivePolicyWindowWithin(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewProactivePolicy(ProactiveOptions{Window: time.Hour})
|
||||
last := time.Now().Add(-30 * time.Minute)
|
||||
if !p.Allow(context.Background(), capsProactive(), last) {
|
||||
t.Error("Allow() = false within window, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProactivePolicyWindowBeyond(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewProactivePolicy(ProactiveOptions{Window: time.Hour})
|
||||
last := time.Now().Add(-90 * time.Minute)
|
||||
if p.Allow(context.Background(), capsProactive(), last) {
|
||||
t.Error("Allow() = true beyond window, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProactivePolicyWindowZeroLastMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewProactivePolicy(ProactiveOptions{Window: time.Hour})
|
||||
if p.Allow(context.Background(), capsProactive(), time.Time{}) {
|
||||
t.Error("Allow() = true with no last message, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProactivePolicyRateLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewProactivePolicy(ProactiveOptions{RateLimit: 3, RateWindow: 10 * time.Second})
|
||||
ctx := context.Background()
|
||||
for i := 1; i <= 3; i++ {
|
||||
if !p.Allow(ctx, capsProactive(), time.Now()) {
|
||||
t.Fatalf("Allow() denied on attempt %d, want allowed under limit", i)
|
||||
}
|
||||
}
|
||||
if p.Allow(ctx, capsProactive(), time.Now()) {
|
||||
t.Error("Allow() allowed past burst limit, want denied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProactivePolicyUnrestrictedChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// telegram/webhook-like channel: proactive capability on, no window, no
|
||||
// rate limit → always allowed.
|
||||
p := NewProactivePolicy(ProactiveOptions{})
|
||||
old := time.Now().Add(-48 * time.Hour)
|
||||
for i := 0; i < 5; i++ {
|
||||
if !p.Allow(context.Background(), capsProactive(), old) {
|
||||
t.Fatalf("Allow() denied on unrestricted channel, want true")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProactivePolicyForPlatform(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
iLink := ProactivePolicyForPlatform("weixin-ilink")
|
||||
now := time.Now()
|
||||
if !iLink.Allow(ctx, capsProactive(), now.Add(-23*time.Hour)) {
|
||||
t.Error("weixin-ilink allowed only within 24h; 23h old should allow")
|
||||
}
|
||||
if iLink.Allow(ctx, capsProactive(), now.Add(-25*time.Hour)) {
|
||||
t.Error("weixin-ilink 25h old last message should deny")
|
||||
}
|
||||
|
||||
oneBot := ProactivePolicyForPlatform("qq-obv11")
|
||||
for i := 0; i < oneBotBurstLimit; i++ {
|
||||
if !oneBot.Allow(ctx, capsProactive(), now) {
|
||||
t.Fatalf("onebot denied on attempt %d, want allowed", i+1)
|
||||
}
|
||||
}
|
||||
if oneBot.Allow(ctx, capsProactive(), now) {
|
||||
t.Error("onebot allowed past burst limit, want denied")
|
||||
}
|
||||
|
||||
tg := ProactivePolicyForPlatform("telegram")
|
||||
if !tg.Allow(ctx, capsProactive(), now.Add(-72*time.Hour)) {
|
||||
t.Error("telegram must push freely, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProactivePolicyNilPolicyAndCanceledCtx(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var p *ProactivePolicy
|
||||
if p.Allow(context.Background(), capsProactive(), time.Now()) {
|
||||
t.Error("nil policy Allow() = true, want false (fail closed)")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
policy := NewProactivePolicy(ProactiveOptions{})
|
||||
if policy.Allow(ctx, capsProactive(), time.Now()) {
|
||||
t.Error("Allow() with canceled ctx = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterSlidingWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
l := NewRateLimiter(2, 50*time.Millisecond)
|
||||
if !l.Allow() {
|
||||
t.Fatal("first Allow() = false, want true")
|
||||
}
|
||||
if !l.Allow() {
|
||||
t.Fatal("second Allow() = false, want true")
|
||||
}
|
||||
if l.Allow() {
|
||||
t.Fatal("third Allow() = true, want false (limit reached)")
|
||||
}
|
||||
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
if !l.Allow() {
|
||||
t.Fatal("Allow() after window elapsed = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterDisabledConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A limiter with no limit/window configured is the safe permissive default.
|
||||
configs := []*RateLimiter{
|
||||
NewRateLimiter(0, 0),
|
||||
NewRateLimiter(-1, time.Second),
|
||||
NewRateLimiter(0, time.Second),
|
||||
}
|
||||
for i, l := range configs {
|
||||
if !l.Allow() || !l.Allow() {
|
||||
t.Errorf("config %d: Allow() = false, want true (unlimited)", i)
|
||||
}
|
||||
}
|
||||
if !(*RateLimiter)(nil).Allow() {
|
||||
t.Error("nil rate limiter Allow() = false, want true (permissive)")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user