Files
xiaoxia-saas/tests/unit/test_auth_handlers.py
T
xiaoxia 8b7510e2bb
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 52s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 43s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m1s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m31s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 47s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m2s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m21s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 1m14s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 50s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 9m42s
AI Code Review / AI Code Review (pull_request) Successful in 6m33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30m46s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m14s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 2m53s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1339h25m51s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1339h49m2s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1339h49m4s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1339h49m6s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1340h16m12s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1340h18m43s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1340h18m45s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1340h18m47s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1340h45m39s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1340h45m41s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1340h45m43s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1340h50m57s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1340h21m18s
style: auto-fix black formatting
2026-07-22 18:23:53 +08:00

244 lines
8.2 KiB
Python
Executable File
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.
"""
JWT + Password 委托层单元测试(第二十一波)
覆盖:
- JWTHandler (create/verify/configure/get)
- PasswordHandler (hash/verify/needs_rehash/validate_strength/configure/get)
"""
from unittest.mock import patch
import pytest
from packages.application.auth.jwt_handler import (
JWTHandler,
configure_jwt_handler,
get_jwt_handler,
)
from packages.application.auth.password_handler import (
PasswordHandler,
configure_password_handler,
get_password_handler,
)
SECRET_KEY = "test-secret-key-for-unit-testing-only-not-for-production"
# ============================================================
# JWTHandler
# ============================================================
class TestJWTHandler:
"""JWTHandler JWT 委托层"""
def test_create_and_verify_access_token(self):
"""创建并验证 access_token"""
handler = JWTHandler(secret_key=SECRET_KEY)
token = handler.create_access_token(user_id="user-123", role="admin")
assert isinstance(token, str)
assert len(token) > 0
payload = handler.verify_access_token(token)
assert payload["sub"] == "user-123"
assert payload["role"] == "admin"
assert "exp" in payload
assert "type" in payload
assert payload["type"] == "access"
def test_create_token_with_additional_claims(self):
"""携带额外 claims"""
handler = JWTHandler(secret_key=SECRET_KEY)
token = handler.create_access_token(
user_id="user-1",
role="user",
additional_claims={"email": "a@b.com", "org_id": "org-1"},
)
payload = handler.verify_access_token(token)
assert payload["email"] == "a@b.com"
assert payload["org_id"] == "org-1"
def test_create_token_default_role(self):
"""默认 role 为空字符串"""
handler = JWTHandler(secret_key=SECRET_KEY)
token = handler.create_access_token(user_id="user-1")
payload = handler.verify_access_token(token)
assert payload["role"] == ""
def test_verify_generic_token(self):
"""verify_token 通用验证方法"""
handler = JWTHandler(secret_key=SECRET_KEY)
token = handler.create_access_token(user_id="user-1")
payload = handler.verify_token(token)
assert payload["sub"] == "user-1"
def test_verify_invalid_token_raises(self):
"""无效 token 验证失败"""
handler = JWTHandler(secret_key=SECRET_KEY)
with pytest.raises(Exception):
handler.verify_access_token("invalid-token")
def test_verify_wrong_secret(self):
"""用不同密钥签名的 token 验证失败"""
handler1 = JWTHandler(secret_key="key-a")
handler2 = JWTHandler(secret_key="key-b")
token = handler1.create_access_token(user_id="user-1")
with pytest.raises(Exception):
handler2.verify_access_token(token)
def test_custom_algorithm(self):
"""自定义算法"""
handler = JWTHandler(secret_key=SECRET_KEY, algorithm="HS256")
token = handler.create_access_token(user_id="user-1")
payload = handler.verify_access_token(token)
assert payload["sub"] == "user-1"
def test_custom_expire_minutes(self):
"""自定义过期时间"""
handler = JWTHandler(secret_key=SECRET_KEY, access_token_expire_minutes=60)
token = handler.create_access_token(user_id="user-1")
payload = handler.verify_access_token(token)
assert payload["sub"] == "user-1"
# ============================================================
# JWTHandler - 全局配置
# ============================================================
class TestJWTGlobalConfig:
"""JWT 全局配置与获取"""
def test_configure_and_get(self):
"""配置后可以获取"""
handler = configure_jwt_handler(secret_key=SECRET_KEY, access_token_expire_minutes=15)
assert isinstance(handler, JWTHandler)
got = get_jwt_handler()
assert got is handler
def test_reconfigure_replaces(self):
"""重新配置会替换"""
h1 = configure_jwt_handler(secret_key="key-a")
h2 = configure_jwt_handler(secret_key="key-b")
assert h1 is not h2
assert get_jwt_handler() is h2
# ============================================================
# PasswordHandler
# ============================================================
class TestPasswordHandler:
"""PasswordHandler 密码委托层"""
def test_hash_and_verify_correct(self):
"""哈希并验证正确密码"""
handler = PasswordHandler()
hashed = handler.hash_password("MySecurePass123")
assert isinstance(hashed, str)
assert hashed != "MySecurePass123"
assert handler.verify_password("MySecurePass123", hashed) is True
def test_verify_wrong_password(self):
"""验证错误密码"""
handler = PasswordHandler()
hashed = handler.hash_password("CorrectPass123")
assert handler.verify_password("WrongPass456", hashed) is False
def test_hash_is_unique_each_time(self):
"""同密码每次哈希不同(salt)"""
handler = PasswordHandler()
h1 = handler.hash_password("SamePass123")
h2 = handler.hash_password("SamePass123")
assert h1 != h2
# 但都能验证通过
assert handler.verify_password("SamePass123", h1)
assert handler.verify_password("SamePass123", h2)
def test_needs_rehash_new_hash(self):
"""新生成的哈希不需要重新计算"""
handler = PasswordHandler()
hashed = handler.hash_password("TestPass123")
assert handler.needs_rehash(hashed) is False
def test_validate_strength_strong(self):
"""强密码校验通过"""
handler = PasswordHandler()
ok, err = handler.validate_strength("StrongPass123")
assert ok is True
assert err is None or err == ""
def test_validate_strength_too_short(self):
"""密码太短"""
handler = PasswordHandler()
ok, err = handler.validate_strength("Ab1")
assert ok is False
assert err is not None
def test_validate_strength_no_uppercase(self):
"""缺少大写字母"""
handler = PasswordHandler()
ok, err = handler.validate_strength("lowercase123")
assert ok is False
assert err is not None
def test_validate_strength_no_lowercase(self):
"""缺少小写字母"""
handler = PasswordHandler()
ok, err = handler.validate_strength("UPPERCASE123")
assert ok is False
assert err is not None
def test_validate_strength_no_digit(self):
"""缺少数字"""
handler = PasswordHandler()
ok, err = handler.validate_strength("NoDigitHere")
assert ok is False
assert err is not None
def test_hash_empty_password(self):
"""空密码哈希报错"""
handler = PasswordHandler()
with pytest.raises((ValueError, Exception)):
handler.hash_password("")
def test_custom_rounds(self):
"""自定义 rounds(用低轮次测试更快)"""
handler = PasswordHandler(rounds=4)
hashed = handler.hash_password("TestPass123")
assert handler.verify_password("TestPass123", hashed)
# ============================================================
# PasswordHandler - 全局配置
# ============================================================
class TestPasswordGlobalConfig:
"""Password 全局配置与获取"""
def test_get_default_handler(self):
"""未配置时 get 返回默认实例"""
# 重置默认实例
with patch("packages.application.auth.password_handler._default_handler", None):
handler = get_password_handler()
assert isinstance(handler, PasswordHandler)
def test_configure_and_get(self):
"""配置后可以获取"""
handler = configure_password_handler(rounds=4)
assert isinstance(handler, PasswordHandler)
got = get_password_handler()
assert got is handler
def test_reconfigure_replaces(self):
"""重新配置会替换"""
h1 = configure_password_handler(rounds=4)
h2 = configure_password_handler(rounds=6)
assert h1 is not h2