Files
xiaoxia-saas/tests/unit/test_redis_session_store.py
T
CI Bot 3921a657e8
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 52s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m27s
CI/CD Pipeline / Unit Tests (push) Failing after 8m24s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m1s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m34s
style: auto-format with black + isort + prettier
2026-07-24 08:47:07 +00:00

388 lines
13 KiB
Python
Executable File

"""Redis Session Store 单元测试"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from packages.adapters.redis.session_store import (
NoopSessionStore,
RedisConfig,
SessionStore,
get_session_store,
)
@pytest.fixture
def mock_redis():
return MagicMock()
@pytest.fixture
def session_store(mock_redis):
return SessionStore(redis_client=mock_redis)
class TestRedisConfig:
"""RedisConfig 默认值测试"""
def test_default_values(self):
"""默认配置"""
cfg = RedisConfig()
assert cfg.HOST == "localhost"
assert cfg.PORT == 6379
assert cfg.DB == 0
assert cfg.PASSWORD is None
assert cfg.DECODE_RESPONSES is True
class TestNoopSessionStore:
"""NoopSessionStore 测试"""
def test_save_session_returns_false(self):
"""保存返回 False"""
store = NoopSessionStore()
assert store.save_session() is False
def test_get_session_returns_none(self):
"""获取返回 None"""
store = NoopSessionStore()
assert store.get_session("sess_123") is None
def test_get_session_by_refresh_token_returns_none(self):
"""通过 refresh_token 获取返回 None"""
store = NoopSessionStore()
assert store.get_session_by_refresh_token("tok_123") is None
def test_get_refresh_token_returns_none(self):
"""获取 refresh_token 返回 None"""
store = NoopSessionStore()
assert store.get_refresh_token("sess_123") is None
def test_update_last_active_returns_false(self):
"""更新活跃时间返回 False"""
store = NoopSessionStore()
assert store.update_last_active("sess_123") is False
def test_delete_session_returns_false(self):
"""删除返回 False"""
store = NoopSessionStore()
assert store.delete_session("sess_123") is False
def test_get_user_sessions_returns_empty(self):
"""用户 session 列表为空"""
store = NoopSessionStore()
assert store.get_user_sessions("user_1") == []
def test_delete_all_user_sessions_returns_zero(self):
"""删除所有返回 0"""
store = NoopSessionStore()
assert store.delete_all_user_sessions("user_1") == 0
def test_session_exists_returns_false(self):
"""存在性检查返回 False"""
store = NoopSessionStore()
assert store.session_exists("sess_123") is False
class TestSessionStoreInit:
"""SessionStore 初始化测试"""
def test_init_with_redis_client(self, mock_redis):
"""使用注入的 redis client"""
store = SessionStore(redis_client=mock_redis)
assert store.redis is mock_redis
def test_init_with_config(self):
"""使用配置创建 redis client"""
cfg = RedisConfig()
cfg.HOST = "redis.example.com"
cfg.PORT = 6380
with patch("packages.adapters.redis.session_store.redis.Redis") as mock_redis_cls:
store = SessionStore(config=cfg)
mock_redis_cls.assert_called_once_with(
host="redis.example.com",
port=6380,
db=0,
password=None,
decode_responses=True,
)
class TestSessionStoreSave:
"""save_session 测试"""
def test_save_success(self, session_store, mock_redis):
"""保存成功"""
result = session_store.save_session(
session_id="sess_001",
user_id="user_001",
refresh_token="refresh_abc",
device_info="Chrome/Windows",
ip_address="192.168.1.1",
)
assert result is True
# 验证 session 数据保存
mock_redis.setex.assert_any_call(
"session:sess_001", 30 * 24 * 60 * 60, mock_redis.setex.call_args_list[0][0][2]
)
# 验证 refresh_token 保存
mock_redis.setex.assert_any_call("refresh_token:sess_001", 30 * 24 * 60 * 60, "refresh_abc")
# 验证反向映射
mock_redis.setex.assert_any_call("refresh_token_map:refresh_abc", 30 * 24 * 60 * 60, "sess_001")
# 验证用户集合
mock_redis.sadd.assert_called_once_with("user_sessions:user_001", "sess_001")
mock_redis.expire.assert_called_once()
def test_save_custom_expiry(self, session_store, mock_redis):
"""自定义过期时间"""
session_store.save_session(
session_id="sess_001",
user_id="user_001",
refresh_token="tok",
device_info="d",
ip_address="1.1.1.1",
expires_in_seconds=3600,
)
# 验证 TTL 为 3600
call_args = mock_redis.setex.call_args_list[0]
assert call_args[0][1] == 3600
def test_save_returns_false_on_error(self, session_store, mock_redis):
"""Redis 异常时返回 False"""
mock_redis.setex.side_effect = Exception("Connection error")
result = session_store.save_session(
session_id="s1",
user_id="u1",
refresh_token="t1",
device_info="d",
ip_address="1.1.1.1",
)
assert result is False
class TestSessionStoreGet:
"""get_session 测试"""
def test_get_existing_session(self, session_store, mock_redis):
"""获取存在的 session"""
import json
session_data = {
"session_id": "sess_001",
"user_id": "user_001",
"device_info": "Chrome",
"ip_address": "1.1.1.1",
}
mock_redis.get.return_value = json.dumps(session_data)
result = session_store.get_session("sess_001")
assert result is not None
assert result["user_id"] == "user_001"
assert result["session_id"] == "sess_001"
mock_redis.get.assert_called_once_with("session:sess_001")
def test_get_nonexistent_session(self, session_store, mock_redis):
"""获取不存在的 session 返回 None"""
mock_redis.get.return_value = None
result = session_store.get_session("nonexistent")
assert result is None
def test_get_returns_none_on_error(self, session_store, mock_redis):
"""Redis 异常返回 None"""
mock_redis.get.side_effect = Exception("error")
result = session_store.get_session("s1")
assert result is None
class TestSessionStoreGetByRefreshToken:
"""get_session_by_refresh_token 测试"""
def test_get_by_refresh_token_success(self, session_store, mock_redis):
"""通过 refresh_token 获取成功"""
import json
session_data = {"session_id": "sess_001", "user_id": "user_001"}
# 第一次调用(反向映射)返回 session_id
# 第二次调用(session数据)返回 json
mock_redis.get.side_effect = ["sess_001", json.dumps(session_data)]
result = session_store.get_session_by_refresh_token("refresh_abc")
assert result is not None
assert result["session_id"] == "sess_001"
def test_get_by_refresh_token_not_found(self, session_store, mock_redis):
"""refresh_token 不存在返回 None"""
mock_redis.get.return_value = None
result = session_store.get_session_by_refresh_token("invalid")
assert result is None
class TestSessionStoreGetRefreshToken:
"""get_refresh_token 测试"""
def test_get_refresh_token_success(self, session_store, mock_redis):
"""获取 refresh_token 成功"""
mock_redis.get.return_value = "refresh_abc"
result = session_store.get_refresh_token("sess_001")
assert result == "refresh_abc"
mock_redis.get.assert_called_once_with("refresh_token:sess_001")
def test_get_refresh_token_not_found(self, session_store, mock_redis):
"""不存在返回 None"""
mock_redis.get.return_value = None
assert session_store.get_refresh_token("sess_001") is None
class TestSessionStoreUpdateLastActive:
"""update_last_active 测试"""
def test_update_success(self, session_store, mock_redis):
"""更新成功"""
import json
session_data = {
"session_id": "sess_001",
"user_id": "user_001",
"last_active_at": "2024-01-01T00:00:00+00:00",
}
mock_redis.get.return_value = json.dumps(session_data)
mock_redis.ttl.return_value = 1800
result = session_store.update_last_active("sess_001")
assert result is True
mock_redis.setex.assert_called_once()
def test_update_session_not_found(self, session_store, mock_redis):
"""session 不存在返回 False"""
mock_redis.get.return_value = None
result = session_store.update_last_active("nonexistent")
assert result is False
def test_update_expired_session(self, session_store, mock_redis):
"""已过期的 session 返回 False"""
import json
session_data = {"session_id": "s1", "user_id": "u1"}
mock_redis.get.return_value = json.dumps(session_data)
mock_redis.ttl.return_value = -2 # 已过期
result = session_store.update_last_active("s1")
assert result is False
class TestSessionStoreDelete:
"""delete_session 测试"""
def test_delete_success(self, session_store, mock_redis):
"""删除成功"""
import json
session_data = {"session_id": "sess_001", "user_id": "user_001"}
mock_redis.get.side_effect = [
json.dumps(session_data), # get_session
"refresh_abc", # get refresh_token
]
result = session_store.delete_session("sess_001")
assert result is True
# 删除 session、refresh_token、反向映射、从用户集合移除
assert mock_redis.delete.call_count >= 3
mock_redis.srem.assert_called_once_with("user_sessions:user_001", "sess_001")
def test_delete_not_found(self, session_store, mock_redis):
"""删除不存在的 session 返回 False"""
mock_redis.get.return_value = None
result = session_store.delete_session("nonexistent")
assert result is False
class TestSessionStoreUserSessions:
"""用户 Session 列表测试"""
def test_get_user_sessions(self, session_store, mock_redis):
"""获取用户所有 session"""
import json
mock_redis.smembers.return_value = {"sess_001", "sess_002"}
session1 = json.dumps({"session_id": "sess_001", "user_id": "u1"})
session2 = json.dumps({"session_id": "sess_002", "user_id": "u1"})
mock_redis.get.side_effect = [session1, session2]
result = session_store.get_user_sessions("user_001")
assert len(result) == 2
def test_get_user_sessions_empty(self, session_store, mock_redis):
"""用户无 session"""
mock_redis.smembers.return_value = set()
result = session_store.get_user_sessions("user_001")
assert result == []
def test_delete_all_user_sessions(self, session_store, mock_redis):
"""删除用户所有 session"""
import json
mock_redis.smembers.return_value = {"sess_001", "sess_002"}
session1 = json.dumps({"session_id": "sess_001", "user_id": "u1"})
session2 = json.dumps({"session_id": "sess_002", "user_id": "u1"})
# get 调用顺序:
# 1-2: get_user_sessions 中两个 session 的 get
# 3-4: delete sess_001 (get_session + get refresh_token)
# 5-6: delete sess_002 (get_session + get refresh_token)
mock_redis.get.side_effect = [
session1,
session2, # get_user_sessions
session1,
"tok1", # delete sess_001
session2,
"tok2", # delete sess_002
]
count = session_store.delete_all_user_sessions("user_001")
assert count == 2
def test_delete_all_empty_user(self, session_store, mock_redis):
"""删除无 session 的用户"""
mock_redis.smembers.return_value = set()
count = session_store.delete_all_user_sessions("user_001")
assert count == 0
class TestSessionStoreExists:
"""session_exists 测试"""
def test_exists_true(self, session_store, mock_redis):
"""存在返回 True"""
mock_redis.exists.return_value = 1
assert session_store.session_exists("sess_001") is True
def test_exists_false(self, session_store, mock_redis):
"""不存在返回 False"""
mock_redis.exists.return_value = 0
assert session_store.session_exists("sess_001") is False
def test_exists_error_returns_false(self, session_store, mock_redis):
"""异常返回 False"""
mock_redis.exists.side_effect = Exception("error")
assert session_store.session_exists("sess_001") is False
class TestGetSessionStore:
"""工厂函数测试"""
def test_disabled_returns_noop(self):
"""禁用返回 NoopSessionStore"""
store = get_session_store(enabled=False)
assert isinstance(store, NoopSessionStore)