172 lines
4.6 KiB
Go
172 lines
4.6 KiB
Go
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)")
|
|
}
|
|
}
|