Files
AskaEth af8591b78a fix(channel,user): owner 车道只认白名单,堵住群主/伪造 role 冒充主人(裁决 06.4.2/06.4.3/06.4.4)
- 适配器只描述平台身份:qq_obv11 的 owner/admin 一律上报 RoleMember,不再上报 RoleOwner
- session 层唯一判主点只认白名单(owner.yaml Accounts ∪ 每渠道 adminIds),nil 档案 fail-closed 拒绝
- 新增「每渠道管理员层」:channels.json ChannelAccount.adminIds[] → runtimeconfig.AdminIDs → OwnerProfile.SetPlatformAdmins(整体替换语义,热重载后移除的 id 立即失效)
- 组合根接线:启动即播种、面板保存先灌白名单再重载适配器(cmd/cyrene/wire_channels.go + 4 条端到端单测)
- 06.4.3 节流与参与度状态按会话独立计数(ConversationKey 三处统一,空会话 GC);06.4.4 未知平台主动发言保守默认 1 条/分钟
2026-10-04 12:07:40 +08:00

198 lines
5.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")
}
}
// TestProactivePolicyForUnknownPlatformIsConservative 是裁决 06.4.4 的断言:未知平台
// (能力未声明、不在平台清单里)**不得**拿到「无限制」——默认给最严格的限频。
func TestProactivePolicyForUnknownPlatformIsConservative(t *testing.T) {
t.Parallel()
ctx := context.Background()
now := time.Now()
for _, platform := range []string{"mystery-im", "discord", ""} {
p := ProactivePolicyForPlatform(platform)
if p == nil {
t.Fatalf("ProactivePolicyForPlatform(%q) = nil", platform)
}
if p.limiter == nil {
t.Fatalf("未知平台 %q 拿到了无限制策略,违反裁决 06.4.4", platform)
}
for i := 0; i < unknownBurstLimit; i++ {
if !p.Allow(ctx, capsProactive(), now) {
t.Fatalf("未知平台 %q 第 %d 条被拒,want allowed(限频内)", platform, i+1)
}
}
if p.Allow(ctx, capsProactive(), now) {
t.Errorf("未知平台 %q 超出保守限频仍被放行", platform)
}
}
}
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)")
}
}