110 lines
4.9 KiB
Python
110 lines
4.9 KiB
Python
"""测试期生产 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"]))
|