Files
Neo-ZQYY/apps/backend/app/ai/test_rate_limiter.py
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

110 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""RateLimiter 单元测试。
被测代码apps/backend/app/ai/rate_limiter.py
纯内存测试,不涉及 DB/网络。
"""
from __future__ import annotations
import time
from unittest.mock import patch
from app.ai.rate_limiter import RateLimiter
class TestCheckUserRate:
"""App1 每用户每分钟限流。"""
def test_allows_under_limit(self):
rl = RateLimiter()
for _ in range(10):
assert rl.check_user_rate("u1", limit=10) is True
def test_rejects_at_limit(self):
rl = RateLimiter()
for _ in range(10):
rl.check_user_rate("u1", limit=10)
assert rl.check_user_rate("u1", limit=10) is False
def test_different_users_independent(self):
rl = RateLimiter()
for _ in range(10):
rl.check_user_rate("u1", limit=10)
# u1 已满u2 不受影响
assert rl.check_user_rate("u1", limit=10) is False
assert rl.check_user_rate("u2", limit=10) is True
def test_window_expiry_allows_again(self):
"""窗口过期后,历史请求不影响当前判断。"""
rl = RateLimiter()
base = time.monotonic()
with patch("app.ai.rate_limiter.time.monotonic", return_value=base):
for _ in range(10):
rl.check_user_rate("u1", limit=10, window_seconds=60)
# 61 秒后,窗口内无请求
with patch("app.ai.rate_limiter.time.monotonic", return_value=base + 61):
assert rl.check_user_rate("u1", limit=10, window_seconds=60) is True
class TestCheckStoreRate:
"""App2~8 每门店每小时限流。"""
def test_allows_under_limit(self):
rl = RateLimiter()
for _ in range(5):
assert rl.check_store_rate(123, limit=5) is True
def test_rejects_at_limit(self):
rl = RateLimiter()
for _ in range(5):
rl.check_store_rate(123, limit=5)
assert rl.check_store_rate(123, limit=5) is False
def test_different_stores_independent(self):
rl = RateLimiter()
for _ in range(5):
rl.check_store_rate(100, limit=5)
assert rl.check_store_rate(100, limit=5) is False
assert rl.check_store_rate(200, limit=5) is True
def test_site_id_int_works(self):
"""site_id 为 int内部转 str 存储。"""
rl = RateLimiter()
assert rl.check_store_rate(2790685415443269, limit=100) is True
def test_window_expiry_allows_again(self):
rl = RateLimiter()
base = time.monotonic()
with patch("app.ai.rate_limiter.time.monotonic", return_value=base):
for _ in range(100):
rl.check_store_rate(123, limit=100, window_seconds=3600)
# 3601 秒后
with patch("app.ai.rate_limiter.time.monotonic", return_value=base + 3601):
assert rl.check_store_rate(123, limit=100, window_seconds=3600) is True
class TestRejectedRequestNotRecorded:
"""被拒绝的请求不应记录时间戳(不占用窗口配额)。"""
def test_rejected_user_request_not_counted(self):
rl = RateLimiter()
for _ in range(3):
rl.check_user_rate("u1", limit=3)
# 连续拒绝不应增加窗口内计数
rl.check_user_rate("u1", limit=3)
rl.check_user_rate("u1", limit=3)
assert len(rl._user_windows["u1"]) == 3
def test_rejected_store_request_not_counted(self):
rl = RateLimiter()
for _ in range(3):
rl.check_store_rate(1, limit=3)
rl.check_store_rate(1, limit=3)
rl.check_store_rate(1, limit=3)
assert len(rl._store_windows["1"]) == 3