Add unit tests for models and services
- Implemented unit tests for Object model including folder and file creation, properties, and path retrieval. - Added unit tests for Setting model covering creation, unique constraints, and type enumeration. - Created unit tests for User model focusing on user creation, uniqueness, and group relationships. - Developed unit tests for Login service to validate login functionality, including 2FA and token generation. - Added utility tests for JWT creation and verification, ensuring token integrity and expiration handling. - Implemented password utility tests for password generation, hashing, and TOTP verification.
This commit is contained in:
5
tests/unit/utils/__init__.py
Normal file
5
tests/unit/utils/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
工具函数单元测试模块
|
||||
|
||||
测试工具类和辅助函数。
|
||||
"""
|
||||
163
tests/unit/utils/test_jwt.py
Normal file
163
tests/unit/utils/test_jwt.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
JWT 工具的单元测试
|
||||
"""
|
||||
import time
|
||||
from datetime import timedelta, datetime, timezone
|
||||
|
||||
import jwt as pyjwt
|
||||
import pytest
|
||||
|
||||
from utils.JWT.JWT import create_access_token, create_refresh_token, SECRET_KEY
|
||||
|
||||
|
||||
# 设置测试用的密钥
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_secret_key():
|
||||
"""为测试设置密钥"""
|
||||
import utils.JWT.JWT as jwt_module
|
||||
jwt_module.SECRET_KEY = "test_secret_key_for_unit_tests"
|
||||
yield
|
||||
# 测试后恢复(虽然在单元测试中不太重要)
|
||||
|
||||
|
||||
def test_create_access_token():
|
||||
"""测试访问令牌创建"""
|
||||
data = {"sub": "testuser", "role": "user"}
|
||||
|
||||
token, expire_time = create_access_token(data)
|
||||
|
||||
assert isinstance(token, str)
|
||||
assert isinstance(expire_time, datetime)
|
||||
|
||||
# 解码验证
|
||||
decoded = pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
assert decoded["sub"] == "testuser"
|
||||
assert decoded["role"] == "user"
|
||||
assert "exp" in decoded
|
||||
|
||||
|
||||
def test_create_access_token_custom_expiry():
|
||||
"""测试自定义过期时间"""
|
||||
data = {"sub": "testuser"}
|
||||
custom_expiry = timedelta(hours=1)
|
||||
|
||||
token, expire_time = create_access_token(data, expires_delta=custom_expiry)
|
||||
|
||||
decoded = pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
|
||||
# 验证过期时间大约是1小时后
|
||||
exp_timestamp = decoded["exp"]
|
||||
now_timestamp = datetime.now(timezone.utc).timestamp()
|
||||
|
||||
# 允许1秒误差
|
||||
assert abs(exp_timestamp - now_timestamp - 3600) < 1
|
||||
|
||||
|
||||
def test_create_refresh_token():
|
||||
"""测试刷新令牌创建"""
|
||||
data = {"sub": "testuser"}
|
||||
|
||||
token, expire_time = create_refresh_token(data)
|
||||
|
||||
assert isinstance(token, str)
|
||||
assert isinstance(expire_time, datetime)
|
||||
|
||||
# 解码验证
|
||||
decoded = pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
assert decoded["sub"] == "testuser"
|
||||
assert decoded["token_type"] == "refresh"
|
||||
assert "exp" in decoded
|
||||
|
||||
|
||||
def test_create_refresh_token_default_expiry():
|
||||
"""测试刷新令牌默认30天过期"""
|
||||
data = {"sub": "testuser"}
|
||||
|
||||
token, expire_time = create_refresh_token(data)
|
||||
|
||||
decoded = pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
|
||||
# 验证过期时间大约是30天后
|
||||
exp_timestamp = decoded["exp"]
|
||||
now_timestamp = datetime.now(timezone.utc).timestamp()
|
||||
|
||||
# 30天 = 30 * 24 * 3600 = 2592000 秒
|
||||
# 允许1秒误差
|
||||
assert abs(exp_timestamp - now_timestamp - 2592000) < 1
|
||||
|
||||
|
||||
def test_token_decode():
|
||||
"""测试令牌解码"""
|
||||
data = {"sub": "user123", "email": "user@example.com"}
|
||||
|
||||
token, _ = create_access_token(data)
|
||||
|
||||
# 解码
|
||||
decoded = pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
|
||||
assert decoded["sub"] == "user123"
|
||||
assert decoded["email"] == "user@example.com"
|
||||
|
||||
|
||||
def test_token_expired():
|
||||
"""测试令牌过期"""
|
||||
data = {"sub": "testuser"}
|
||||
|
||||
# 创建一个立即过期的令牌
|
||||
token, _ = create_access_token(data, expires_delta=timedelta(seconds=-1))
|
||||
|
||||
# 尝试解码应该抛出过期异常
|
||||
with pytest.raises(pyjwt.ExpiredSignatureError):
|
||||
pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
|
||||
|
||||
def test_token_invalid_signature():
|
||||
"""测试无效签名"""
|
||||
data = {"sub": "testuser"}
|
||||
|
||||
token, _ = create_access_token(data)
|
||||
|
||||
# 使用错误的密钥解码
|
||||
with pytest.raises(pyjwt.InvalidSignatureError):
|
||||
pyjwt.decode(token, "wrong_secret_key", algorithms=["HS256"])
|
||||
|
||||
|
||||
def test_access_token_does_not_have_token_type():
|
||||
"""测试访问令牌不包含 token_type"""
|
||||
data = {"sub": "testuser"}
|
||||
|
||||
token, _ = create_access_token(data)
|
||||
|
||||
decoded = pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
|
||||
assert "token_type" not in decoded
|
||||
|
||||
|
||||
def test_refresh_token_has_token_type():
|
||||
"""测试刷新令牌包含 token_type"""
|
||||
data = {"sub": "testuser"}
|
||||
|
||||
token, _ = create_refresh_token(data)
|
||||
|
||||
decoded = pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
|
||||
assert decoded["token_type"] == "refresh"
|
||||
|
||||
|
||||
def test_token_payload_preserved():
|
||||
"""测试自定义负载保留"""
|
||||
data = {
|
||||
"sub": "user123",
|
||||
"name": "Test User",
|
||||
"roles": ["admin", "user"],
|
||||
"metadata": {"key": "value"}
|
||||
}
|
||||
|
||||
token, _ = create_access_token(data)
|
||||
|
||||
decoded = pyjwt.decode(token, "test_secret_key_for_unit_tests", algorithms=["HS256"])
|
||||
|
||||
assert decoded["sub"] == "user123"
|
||||
assert decoded["name"] == "Test User"
|
||||
assert decoded["roles"] == ["admin", "user"]
|
||||
assert decoded["metadata"] == {"key": "value"}
|
||||
138
tests/unit/utils/test_password.py
Normal file
138
tests/unit/utils/test_password.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Password 工具类的单元测试
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from utils.password.pwd import Password, PasswordStatus
|
||||
|
||||
|
||||
def test_password_generate_default_length():
|
||||
"""测试默认长度生成密码"""
|
||||
password = Password.generate()
|
||||
|
||||
# 默认长度为 8,token_hex 生成的是16进制字符串,长度是原始长度的2倍
|
||||
assert len(password) == 16
|
||||
assert isinstance(password, str)
|
||||
|
||||
|
||||
def test_password_generate_custom_length():
|
||||
"""测试自定义长度生成密码"""
|
||||
length = 12
|
||||
password = Password.generate(length=length)
|
||||
|
||||
assert len(password) == length * 2
|
||||
assert isinstance(password, str)
|
||||
|
||||
|
||||
def test_password_hash():
|
||||
"""测试密码哈希"""
|
||||
plain_password = "my_secure_password_123"
|
||||
hashed = Password.hash(plain_password)
|
||||
|
||||
assert hashed != plain_password
|
||||
assert isinstance(hashed, str)
|
||||
# Argon2 哈希以 $argon2 开头
|
||||
assert hashed.startswith("$argon2")
|
||||
|
||||
|
||||
def test_password_verify_valid():
|
||||
"""测试正确密码验证"""
|
||||
plain_password = "correct_password"
|
||||
hashed = Password.hash(plain_password)
|
||||
|
||||
status = Password.verify(hashed, plain_password)
|
||||
|
||||
assert status == PasswordStatus.VALID
|
||||
|
||||
|
||||
def test_password_verify_invalid():
|
||||
"""测试错误密码验证"""
|
||||
plain_password = "correct_password"
|
||||
wrong_password = "wrong_password"
|
||||
hashed = Password.hash(plain_password)
|
||||
|
||||
status = Password.verify(hashed, wrong_password)
|
||||
|
||||
assert status == PasswordStatus.INVALID
|
||||
|
||||
|
||||
def test_password_verify_expired():
|
||||
"""测试密码哈希过期检测"""
|
||||
# 注意: 实际检测需要修改 Argon2 参数,这里只是测试接口
|
||||
# 在真实场景中,当哈希参数过时时会返回 EXPIRED
|
||||
plain_password = "password"
|
||||
hashed = Password.hash(plain_password)
|
||||
|
||||
status = Password.verify(hashed, plain_password)
|
||||
|
||||
# 新生成的哈希应该是 VALID
|
||||
assert status in [PasswordStatus.VALID, PasswordStatus.EXPIRED]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_totp_generate():
|
||||
"""测试 TOTP 密钥生成"""
|
||||
username = "testuser"
|
||||
|
||||
response = await Password.generate_totp(username)
|
||||
|
||||
assert response.setup_token is not None
|
||||
assert response.uri is not None
|
||||
assert isinstance(response.setup_token, str)
|
||||
assert isinstance(response.uri, str)
|
||||
# TOTP URI 格式: otpauth://totp/...
|
||||
assert response.uri.startswith("otpauth://totp/")
|
||||
assert username in response.uri
|
||||
|
||||
|
||||
def test_totp_verify_valid():
|
||||
"""测试 TOTP 验证正确"""
|
||||
import pyotp
|
||||
|
||||
# 生成密钥
|
||||
secret = pyotp.random_base32()
|
||||
|
||||
# 生成当前有效的验证码
|
||||
totp = pyotp.TOTP(secret)
|
||||
valid_code = totp.now()
|
||||
|
||||
# 验证
|
||||
status = Password.verify_totp(secret, valid_code)
|
||||
|
||||
assert status == PasswordStatus.VALID
|
||||
|
||||
|
||||
def test_totp_verify_invalid():
|
||||
"""测试 TOTP 验证错误"""
|
||||
import pyotp
|
||||
|
||||
secret = pyotp.random_base32()
|
||||
invalid_code = "000000" # 几乎不可能是当前有效码
|
||||
|
||||
status = Password.verify_totp(secret, invalid_code)
|
||||
|
||||
# 注意: 极小概率 000000 恰好是有效码,但实际测试中基本不会发生
|
||||
assert status == PasswordStatus.INVALID
|
||||
|
||||
|
||||
def test_password_hash_consistency():
|
||||
"""测试相同密码多次哈希结果不同(盐随机)"""
|
||||
password = "test_password"
|
||||
|
||||
hash1 = Password.hash(password)
|
||||
hash2 = Password.hash(password)
|
||||
|
||||
# 由于盐是随机的,两次哈希结果应该不同
|
||||
assert hash1 != hash2
|
||||
|
||||
# 但都应该能通过验证
|
||||
assert Password.verify(hash1, password) == PasswordStatus.VALID
|
||||
assert Password.verify(hash2, password) == PasswordStatus.VALID
|
||||
|
||||
|
||||
def test_password_generate_uniqueness():
|
||||
"""测试生成的密码唯一性"""
|
||||
passwords = [Password.generate() for _ in range(100)]
|
||||
|
||||
# 100个密码应该都不相同
|
||||
assert len(set(passwords)) == 100
|
||||
Reference in New Issue
Block a user