Files
cma-management/backend/tests/test_redis_token_isolation_20260918.py

110 lines
4.9 KiB
Python
Raw Permalink 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.
"""测试期生产 Redis 隔离 —— 回归断言(project-20260918-P1-test-redis-token-fix)
背景:测试套件通过真实 /api/cma/auth/login 签发 token,而 app.auth_middleware 在 import 时
连接 127.0.0.1:6379 db=1(生产 Redis)、ping 成功即 _redis_available=True → create_token()
把 token setex 进生产库(TTL 24h)。测试库首个用户 id=1 与生产 users.id=1 撞号,且
require_auth() 只按 user_id 反查用户,故每轮全量测试都会留下可直接使用的生产会话 token。
本文件断言隔离生效(可复现、无副作用):
1) 测试期 auth_middleware / utils.cache 的 Redis 通道均关闭(客户端对象为 None);
2) 生产 Redis 客户端构造被 conftest 阻断器全部拦下(计数 ≥ 2:auth_middleware + cache);
3) 登录签发的 token 只落进程内 _token_store,且经 require_auth 仍可正常鉴权(语义不变)。
"""
import json
import pytest
from app import auth_middleware
from app.utils import cache as cache_util
from tests.conftest import create_test_user, get_token_for_user, auth_header
def test_auth_middleware_redis_channel_is_off_in_tests():
"""测试期 auth_middleware 必须走进程内 _token_store 降级分支(生产 Redis 零写入的前提)"""
assert auth_middleware._redis_available is False
assert auth_middleware._redis is None
def test_cache_redis_channel_is_off_in_tests():
"""测试夹具会调 cache.delete('ai'),同源 db=1 —— 一并关闭,保证整轮零写命令"""
assert cache_util._available is False
assert cache_util._client is None
def test_production_redis_constructor_is_blocked(production_redis_isolation_state):
"""redis.Redis 构造器被阻断器替换:导入期的两次构造尝试全部被拒(无生产连接)"""
state = production_redis_isolation_state
assert state["constructor_attempts"] >= 2, state
# 阻断即无客户端实例 → 不存在可用的生产 Redis 通道
assert auth_middleware._redis is None
assert cache_util._client is None
def test_login_token_lands_in_process_store_only(db, client):
"""登录 token 只落进程内 _token_store(不产生任何生产 Redis 键)"""
user = create_test_user(db)
token = get_token_for_user(client)
assert token and len(token) == 64
assert token in auth_middleware._token_store
assert json.loads(auth_middleware._token_store[token]) == {
"user_id": user.id,
"entity_id": 1,
}
# token 只能从进程内存储解析(Redis 通道关闭)
assert auth_middleware._resolve_token_data(token) == {"user_id": user.id, "entity_id": 1}
assert auth_middleware._resolve_user_id(token) == user.id
def test_process_store_token_still_authenticates(db, client):
"""降级分支不影响鉴权语义:进程内 token 经 require_auth 正常通过(/me 200)"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.get("/api/cma/auth/me", headers=auth_header(token))
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["username"] == "testadmin"
assert body["role"] == "ceo"
assert body["entity_id"] == 1
# 无效 token 仍按原语义 401(隔离未放宽鉴权)
assert client.get("/api/cma/auth/me", headers=auth_header("0" * 64)).status_code == 401
def test_create_token_writes_process_store_not_redis(db):
"""create_token() 直接调用:写入进程内存储、解析一致、Redis 侧无客户端可写"""
user = create_test_user(db, username="redis_iso_user")
token = auth_middleware.create_token(user.id, 1)
assert token in auth_middleware._token_store
assert auth_middleware.get_token_entity_id(token) == 1
assert auth_middleware._redis is None, "测试期不应存在生产 Redis 客户端实例"
def test_token_store_is_process_local(db, client):
"""token 仅存于本进程:模拟另一个进程/worker 的独立存储看不到它(跨 worker 不可见即无共享 Redis)"""
create_test_user(db)
token = get_token_for_user(client)
fresh_store: dict = {} # 模拟另一个 worker 的独立进程内存储
assert token not in fresh_store
assert token in auth_middleware._token_store # 本进程可见
# 有 Redis 通道时 token 会在生产库跨 worker 共享 —— 这正是被本单关闭的路径
assert auth_middleware._redis is None
def test_production_redis_boundary_invariant():
"""不变量:只要 _redis_available 为 False,create_token 不可能触达 Redis(唯一写入分支)"""
assert auth_middleware._redis_available is False
# 观察既有代码结构:写入分支受 _redis_available 单点控制(本断言防未来改动绕开开关)
import inspect
src = inspect.getsource(auth_middleware.create_token)
assert "if _redis_available:" in src
assert src.count("_redis.setex(") == 1
if __name__ == "__main__": # pragma: no cover
raise SystemExit(pytest.main([__file__, "-q"]))