feat: comprehensive test infrastructure improvements
- Fix unit test imports: add env setup in conftest.py before module imports - Add 24 new auth router tests (RBAC, preferences, password validation) - Add 16 new tests for litellm and chat_summary routers - Apply black formatting and ruff linting across codebase - Add pre-commit hooks configuration (black, ruff, file checks) - Increase CI coverage threshold from 40% to 50% Test Results: - 206 tests passing (91 unit + 115 integration) - Coverage: 58.79% on core modules - auth.py: 57% → 85%, litellm.py: 23% → 87%, chat_summary.py: 41% → 100% - auth_service: 96.51%, config: 100%, rbac: 93.48%
This commit is contained in:
@@ -1,30 +1,33 @@
|
||||
"""
|
||||
Unit tests for auth.py - JWT, TOTP, password hashing, encryption.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import jwt
|
||||
import pyotp
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from auth_service import (
|
||||
verify_password,
|
||||
get_password_hash,
|
||||
_decrypt,
|
||||
_encrypt,
|
||||
clear_passkey_credentials,
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
user_from_access_token,
|
||||
verify_totp,
|
||||
load_totp_secret,
|
||||
save_totp_secret,
|
||||
load_password_hash,
|
||||
save_password_hash,
|
||||
get_password_hash,
|
||||
increment_token_version,
|
||||
verify_token_version,
|
||||
_encrypt,
|
||||
_decrypt,
|
||||
load_passkey_credentials,
|
||||
load_password_hash,
|
||||
load_totp_secret,
|
||||
save_passkey_credential,
|
||||
clear_passkey_credentials,
|
||||
save_password_hash,
|
||||
save_totp_secret,
|
||||
user_from_access_token,
|
||||
verify_password,
|
||||
verify_token_version,
|
||||
verify_totp,
|
||||
)
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
class TestPasswordHashing:
|
||||
@@ -79,8 +82,8 @@ class TestJWTTokens:
|
||||
token = create_access_token(data, expires_delta)
|
||||
|
||||
payload = jwt.decode(token, mock_config.SECRET_KEY, algorithms=[mock_config.ALGORITHM])
|
||||
exp_time = datetime.fromtimestamp(payload["exp"], tz=timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
exp_time = datetime.fromtimestamp(payload["exp"], tz=UTC)
|
||||
now = datetime.now(UTC)
|
||||
|
||||
# Should expire in approximately 60 minutes
|
||||
time_diff = (exp_time - now).total_seconds()
|
||||
@@ -111,10 +114,7 @@ class TestJWTTokens:
|
||||
|
||||
def test_user_from_expired_token(self, mock_config, temp_rbac_file):
|
||||
"""Test that expired token raises HTTPException."""
|
||||
token = create_access_token(
|
||||
{"sub": "testuser", "role": "admin"},
|
||||
expires_delta=timedelta(seconds=-1)
|
||||
)
|
||||
token = create_access_token({"sub": "testuser", "role": "admin"}, expires_delta=timedelta(seconds=-1))
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
user_from_access_token(token)
|
||||
@@ -141,9 +141,9 @@ class TestJWTTokens:
|
||||
"""Test that token without username fails."""
|
||||
# Create token without 'sub' field
|
||||
token = jwt.encode(
|
||||
{"role": "admin", "type": "access", "exp": datetime.now(timezone.utc) + timedelta(minutes=30)},
|
||||
{"role": "admin", "type": "access", "exp": datetime.now(UTC) + timedelta(minutes=30)},
|
||||
mock_config.SECRET_KEY,
|
||||
algorithm=mock_config.ALGORITHM
|
||||
algorithm=mock_config.ALGORITHM,
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
@@ -209,7 +209,9 @@ class TestTOTP:
|
||||
|
||||
monkeypatch.setenv("TOTP_SECRET", "ENV_SECRET_KEY")
|
||||
import importlib
|
||||
|
||||
import config
|
||||
|
||||
importlib.reload(config)
|
||||
|
||||
# When no secret in file, should return env var
|
||||
@@ -237,7 +239,9 @@ class TestTOTP:
|
||||
# Also clear environment variable fallback
|
||||
monkeypatch.setenv("TOTP_SECRET", "")
|
||||
import importlib
|
||||
|
||||
import config
|
||||
|
||||
importlib.reload(config)
|
||||
# No secret saved, should return True (2FA disabled)
|
||||
assert verify_totp("any_code")
|
||||
@@ -248,10 +252,11 @@ class TestTOTP:
|
||||
|
||||
# Ensure auth file is properly initialized with last_totp_ts
|
||||
import json
|
||||
with open(temp_auth_file, 'r') as f:
|
||||
|
||||
with open(temp_auth_file) as f:
|
||||
data = json.load(f)
|
||||
data['last_totp_ts'] = 0
|
||||
with open(temp_auth_file, 'w') as f:
|
||||
data["last_totp_ts"] = 0
|
||||
with open(temp_auth_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
totp = pyotp.TOTP(sample_totp_secret)
|
||||
@@ -280,15 +285,18 @@ class TestPasswordPersistence:
|
||||
"""Test loading password hash from environment variable."""
|
||||
# Clear any hash in the file first
|
||||
import json
|
||||
with open(temp_auth_file, 'r') as f:
|
||||
|
||||
with open(temp_auth_file) as f:
|
||||
data = json.load(f)
|
||||
data['password_hash'] = ""
|
||||
with open(temp_auth_file, 'w') as f:
|
||||
data["password_hash"] = ""
|
||||
with open(temp_auth_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
# Reload auth_service to clear any cached data
|
||||
import auth_service
|
||||
import importlib
|
||||
|
||||
import auth_service
|
||||
|
||||
importlib.reload(auth_service)
|
||||
|
||||
# When no hash in file, should return env var
|
||||
|
||||
Reference in New Issue
Block a user