package autoassignment import ( "context" "testing" "time" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // setupTestRedis 创建一个基于 miniredis 的测试 Redis 环境。 // 返回 miniredis 服务实例和 redis.Client。 // miniredis.RunT 会自动在 t.Cleanup 中关闭服务。 func setupTestRedis(t *testing.T) (*miniredis.Miniredis, *redis.Client) { t.Helper() mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{ Addr: mr.Addr(), }) t.Cleanup(func() { rdb.Close() }) return mr, rdb } // --- IsAllowed 测试 --- // TestIsAllowed_首次请求允许 验证:新窗口中没有任何计数时,IsAllowed 应返回 true。 func TestIsAllowed_首次请求允许(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) limit := 3 windowSeconds := 300 // 新窗口中没有任何计数,应该允许 assert.True(t, rl.IsAllowed(ctx, inboxID, agentID, limit, windowSeconds), "首次请求应被允许") } // TestIsAllowed_达到限制后拒绝 验证:当计数达到 limit 后,IsAllowed 应返回 false。 func TestIsAllowed_达到限制后拒绝(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) limit := 3 windowSeconds := 300 // 先 Increment 3 次,达到限制 for i := 0; i < limit; i++ { err := rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) } // 达到限制后,应该拒绝 assert.False(t, rl.IsAllowed(ctx, inboxID, agentID, limit, windowSeconds), "达到限制后应被拒绝") } // TestIsAllowed_未达到限制时允许 验证:计数小于 limit 时,IsAllowed 应返回 true。 func TestIsAllowed_未达到限制时允许(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) limit := 3 windowSeconds := 300 // Increment 2 次,未达到限制 for i := 0; i < limit-1; i++ { err := rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) } // 未达到限制,应该允许 assert.True(t, rl.IsAllowed(ctx, inboxID, agentID, limit, windowSeconds), "未达到限制时应被允许") } // TestIsAllowed_Limit为零时总是允许 验证:limit <= 0 时,IsAllowed 应返回 true(不限制)。 func TestIsAllowed_Limit为零时总是允许(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() // limit = 0,应总是允许 assert.True(t, rl.IsAllowed(ctx, 1, 10, 0, 300), "limit 为 0 时应总是允许") // limit = -1,应总是允许 assert.True(t, rl.IsAllowed(ctx, 1, 10, -1, 300), "limit 为负数时应总是允许") } // TestIsAllowed_不同Agent独立计数 验证:不同 agent 的限制独立计算。 func TestIsAllowed_不同Agent独立计数(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) limit := 2 windowSeconds := 300 // Agent A 达到限制 for i := 0; i < limit; i++ { err := rl.Increment(ctx, inboxID, 10, windowSeconds) require.NoError(t, err) } assert.False(t, rl.IsAllowed(ctx, inboxID, 10, limit, windowSeconds), "Agent A 达到限制后应被拒绝") // Agent B 未达到限制,应允许 assert.True(t, rl.IsAllowed(ctx, inboxID, 20, limit, windowSeconds), "Agent B 应独立计算,首次请求应被允许") } // TestIsAllowed_不同Inbox独立计数 验证:不同 inbox 的限制独立计算。 func TestIsAllowed_不同Inbox独立计数(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() agentID := uint(10) limit := 2 windowSeconds := 300 // Inbox 1 达到限制 for i := 0; i < limit; i++ { err := rl.Increment(ctx, 1, agentID, windowSeconds) require.NoError(t, err) } assert.False(t, rl.IsAllowed(ctx, 1, agentID, limit, windowSeconds), "Inbox 1 达到限制后应被拒绝") // Inbox 2 未达到限制,应允许 assert.True(t, rl.IsAllowed(ctx, 2, agentID, limit, windowSeconds), "Inbox 2 应独立计算,首次请求应被允许") } // --- Increment 测试 --- // TestIncrement_正常计数增加 验证:每次 Increment 后计数应递增 1。 func TestIncrement_正常计数增加(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) windowSeconds := 300 // 验证初始计数为 0 count, err := rl.GetCount(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) assert.Equal(t, 0, count) // Increment 1 次 err = rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) count, err = rl.GetCount(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) assert.Equal(t, 1, count) // Increment 第 2 次 err = rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) count, err = rl.GetCount(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) assert.Equal(t, 2, count) } // TestIncrement_设置TTL 验证:Increment 后 key 应有正确的 TTL。 func TestIncrement_设置TTL(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) windowSeconds := 300 err := rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) // 验证 key 存在且有 TTL key := rl.key(inboxID, agentID, windowSeconds) ttl, err := rdb.TTL(ctx, key).Result() require.NoError(t, err) // TTL 应大于 0,且不超过 windowSeconds assert.Greater(t, ttl, time.Duration(0)) assert.LessOrEqual(t, ttl, time.Duration(windowSeconds)*time.Second) } // TestIncrement_重复Increment不覆盖TTL 验证:窗口内后续 Increment 不应重新设置 TTL。 func TestIncrement_重复Increment不覆盖TTL(t *testing.T) { mr, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) windowSeconds := 60 // 第一次 Increment,设置 TTL err := rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) key := rl.key(inboxID, agentID, windowSeconds) ttl1, err := rdb.TTL(ctx, key).Result() require.NoError(t, err) // 快进时间,使 TTL 减少 mr.FastForward(30 * time.Second) ttlAfterForward, err := rdb.TTL(ctx, key).Result() require.NoError(t, err) assert.Less(t, ttlAfterForward, ttl1, "快进后 TTL 应减少") // 第二次 Increment,不应重新设置 TTL(TTL 应继续减少而非恢复) err = rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) ttl2, err := rdb.TTL(ctx, key).Result() require.NoError(t, err) assert.Less(t, ttl2, ttl1, "后续 Increment 不应重新设置 TTL") } // --- Redis 错误降级测试 --- // TestIsAllowed_Redis错误时允许 验证:当 Redis 出错时,IsAllowed 应返回 true(降级允许)。 func TestIsAllowed_Redis错误时允许(t *testing.T) { mr, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) limit := 1 windowSeconds := 300 // 先 Increment 1 次,达到限制 err := rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) assert.False(t, rl.IsAllowed(ctx, inboxID, agentID, limit, windowSeconds), "正常情况下达到限制应拒绝") // 关闭 miniredis,模拟 Redis 连接错误 mr.Close() // Redis 错误时应降级允许 assert.True(t, rl.IsAllowed(ctx, inboxID, agentID, limit, windowSeconds), "Redis 错误时应降级允许请求") } // TestIncrement_Redis错误返回错误 验证:当 Redis 出错时,Increment 应返回错误。 func TestIncrement_Redis错误返回错误(t *testing.T) { mr, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) windowSeconds := 300 // 关闭 miniredis,模拟 Redis 连接错误 mr.Close() err := rl.Increment(ctx, inboxID, agentID, windowSeconds) assert.Error(t, err, "Redis 错误时 Increment 应返回错误") } // --- 窗口过期后限制重置测试 --- // TestIsAllowed_窗口过期后限制重置 验证:时间窗口过期后,计数应重置,请求应重新被允许。 func TestIsAllowed_窗口过期后限制重置(t *testing.T) { mr, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) limit := 2 windowSeconds := 60 // Increment 达到限制 for i := 0; i < limit; i++ { err := rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) } assert.False(t, rl.IsAllowed(ctx, inboxID, agentID, limit, windowSeconds), "达到限制后应被拒绝") // 快进时间超过窗口时长,使 key 过期 mr.FastForward(time.Duration(windowSeconds) * time.Second) // 窗口过期后,应重新允许 assert.True(t, rl.IsAllowed(ctx, inboxID, agentID, limit, windowSeconds), "窗口过期后应重新允许请求") } // --- GetCount 测试 --- // TestGetCount_无计数时返回零 验证:没有 Increment 时 GetCount 应返回 0。 func TestGetCount_无计数时返回零(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) windowSeconds := 300 count, err := rl.GetCount(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) assert.Equal(t, 0, count, "无计数时应返回 0") } // TestGetCount_正确返回计数 验证:Increment 后 GetCount 应返回正确的计数。 func TestGetCount_正确返回计数(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) agentID := uint(10) windowSeconds := 300 for i := 1; i <= 5; i++ { err := rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) count, err := rl.GetCount(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) assert.Equal(t, i, count) } } // --- Reset 测试 --- // TestReset_清除所有计数 验证:Reset 应清除指定 inbox 下所有 agent 的计数。 func TestReset_清除所有计数(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() inboxID := uint(1) windowSeconds := 300 // 为多个 agent Increment for agentID := uint(10); agentID <= 13; agentID++ { err := rl.Increment(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) } // 验证计数存在 for agentID := uint(10); agentID <= 13; agentID++ { count, err := rl.GetCount(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) assert.Equal(t, 1, count) } // Reset inbox err := rl.Reset(ctx, inboxID) require.NoError(t, err) // 验证计数已被清除 for agentID := uint(10); agentID <= 13; agentID++ { count, err := rl.GetCount(ctx, inboxID, agentID, windowSeconds) require.NoError(t, err) assert.Equal(t, 0, count, "Reset 后计数应被清除") } } // TestReset_不影响其他Inbox 验证:Reset 一个 inbox 不应影响其他 inbox 的计数。 func TestReset_不影响其他Inbox(t *testing.T) { _, rdb := setupTestRedis(t) rl := NewRateLimiter(rdb) ctx := context.Background() windowSeconds := 300 // Inbox 1 和 Inbox 2 都有计数 err := rl.Increment(ctx, 1, 10, windowSeconds) require.NoError(t, err) err = rl.Increment(ctx, 2, 10, windowSeconds) require.NoError(t, err) // Reset Inbox 1 err = rl.Reset(ctx, 1) require.NoError(t, err) // Inbox 1 计数被清除 count, err := rl.GetCount(ctx, 1, 10, windowSeconds) require.NoError(t, err) assert.Equal(t, 0, count) // Inbox 2 计数不受影响 count, err = rl.GetCount(ctx, 2, 10, windowSeconds) require.NoError(t, err) assert.Equal(t, 1, count, "其他 inbox 的计数不应受影响") } // --- ParseAgentID 测试 --- // TestParseAgentID_正常解析 验证:ParseAgentID 应正确解析合法字符串。 func TestParseAgentID_正常解析(t *testing.T) { id, err := ParseAgentID("42") assert.NoError(t, err) assert.Equal(t, uint(42), id) } // TestParseAgentID_非法字符串 验证:ParseAgentID 应对非法字符串返回错误。 func TestParseAgentID_非法字符串(t *testing.T) { _, err := ParseAgentID("abc") assert.Error(t, err) } // TestParseAgentID_空字符串 验证:ParseAgentID 应对空字符串返回错误。 func TestParseAgentID_空字符串(t *testing.T) { _, err := ParseAgentID("") assert.Error(t, err) } // TestParseAgentID_负数 验证:ParseAgentID 应对负数字符串返回错误。 func TestParseAgentID_负数(t *testing.T) { _, err := ParseAgentID("-1") assert.Error(t, err) } // TestParseAgentID_零 验证:ParseAgentID 应正确解析 "0"。 func TestParseAgentID_零(t *testing.T) { id, err := ParseAgentID("0") assert.NoError(t, err) assert.Equal(t, uint(0), id) }