""" Unit Tests - Security Utils - ServiceManagerWeb Tests para app.core.security: hash de passwords, JWT tokens y TOTP. No requieren base de datos ni red. """ import pytest from datetime import timedelta # ============================================================ # PASSWORD HASHING # ============================================================ class TestPasswordHashing: """Tests para hash y verificación de contraseñas.""" def test_hash_password_returns_string(self): """El hash debe retornar un string.""" from app.core.security import SecurityUtils result = SecurityUtils.hash_password("MyPassword123!") assert isinstance(result, str) def test_hash_is_not_plain_password(self): """El hash no debe ser igual al password original.""" from app.core.security import SecurityUtils password = "MyPassword123!" hashed = SecurityUtils.hash_password(password) assert hashed != password def test_verify_correct_password(self): """Verificar password correcto debe retornar True.""" from app.core.security import SecurityUtils password = "CorrectPassword99!" hashed = SecurityUtils.hash_password(password) assert SecurityUtils.verify_password(password, hashed) is True def test_verify_wrong_password(self): """Verificar password incorrecto debe retornar False.""" from app.core.security import SecurityUtils password = "CorrectPassword99!" hashed = SecurityUtils.hash_password(password) assert SecurityUtils.verify_password("WrongPassword!", hashed) is False def test_two_hashes_of_same_password_are_different(self): """Cada hash debe ser único (salt diferente).""" from app.core.security import SecurityUtils password = "SamePassword123!" hash1 = SecurityUtils.hash_password(password) hash2 = SecurityUtils.hash_password(password) assert hash1 != hash2 def test_verify_empty_password_against_hash(self): """Verificar string vacío contra hash de otra contraseña debe fallar.""" from app.core.security import SecurityUtils hashed = SecurityUtils.hash_password("SomePassword!") assert SecurityUtils.verify_password("", hashed) is False # ============================================================ # JWT ACCESS TOKENS # ============================================================ class TestAccessTokens: """Tests para creación y verificación de JWT access tokens.""" def test_create_access_token_returns_string(self): """create_access_token debe retornar un string.""" from app.core.security import SecurityUtils token = SecurityUtils.create_access_token(data={"sub": "user-123"}) assert isinstance(token, str) assert len(token) > 20 def test_verify_valid_access_token(self): """Un token válido debe retornar el payload.""" from app.core.security import SecurityUtils payload_in = {"sub": "user-abc", "role": "AGENT"} token = SecurityUtils.create_access_token(data=payload_in) payload_out = SecurityUtils.verify_token(token) assert payload_out is not None assert payload_out["sub"] == "user-abc" assert payload_out["role"] == "AGENT" def test_verify_invalid_token_returns_none(self): """Un token inválido debe retornar None.""" from app.core.security import SecurityUtils result = SecurityUtils.verify_token("this.is.not.a.valid.token") assert result is None def test_verify_tampered_token_returns_none(self): """Un token modificado debe retornar None.""" from app.core.security import SecurityUtils token = SecurityUtils.create_access_token(data={"sub": "user-123"}) # Modificar el token parts = token.split(".") tampered = parts[0] + "." + parts[1] + "XXXXX." + parts[2] assert SecurityUtils.verify_token(tampered) is None def test_create_token_with_custom_expiry(self): """Token con expiración personalizada debe ser verificable.""" from app.core.security import SecurityUtils token = SecurityUtils.create_access_token( data={"sub": "user-xyz"}, expires_delta=timedelta(minutes=30) ) payload = SecurityUtils.verify_token(token) assert payload is not None assert payload["sub"] == "user-xyz" def test_expired_token_returns_none(self): """Token expirado debe retornar None.""" from app.core.security import SecurityUtils token = SecurityUtils.create_access_token( data={"sub": "user-exp"}, expires_delta=timedelta(seconds=-1) # Expirado en el pasado ) result = SecurityUtils.verify_token(token) assert result is None # ============================================================ # JWT REFRESH TOKENS # ============================================================ class TestRefreshTokens: """Tests para creación de refresh tokens.""" def test_create_refresh_token_returns_string(self): """create_refresh_token debe retornar un string.""" from app.core.security import SecurityUtils token = SecurityUtils.create_refresh_token(data={"sub": "user-456"}) assert isinstance(token, str) def test_refresh_token_has_type_field(self): """El refresh token debe contener el campo type=refresh.""" from app.core.security import SecurityUtils token = SecurityUtils.create_refresh_token(data={"sub": "user-456"}) payload = SecurityUtils.verify_token(token) assert payload is not None assert payload.get("type") == "refresh" def test_refresh_token_preserves_subject(self): """El refresh token debe preservar el campo sub.""" from app.core.security import SecurityUtils token = SecurityUtils.create_refresh_token(data={"sub": "user-999"}) payload = SecurityUtils.verify_token(token) assert payload["sub"] == "user-999" # ============================================================ # TOTP / 2FA # ============================================================ class TestTOTP: """Tests para generación y verificación de TOTP.""" def test_generate_totp_secret_returns_string(self): """generate_totp_secret debe retornar un string base32.""" from app.core.security import SecurityUtils secret = SecurityUtils.generate_totp_secret() assert isinstance(secret, str) assert len(secret) > 0 def test_two_secrets_are_different(self): """Dos secrets consecutivos deben ser distintos.""" from app.core.security import SecurityUtils secret1 = SecurityUtils.generate_totp_secret() secret2 = SecurityUtils.generate_totp_secret() assert secret1 != secret2 def test_verify_valid_totp_code(self): """Un código TOTP válido debe verificarse correctamente.""" import pyotp from app.core.security import SecurityUtils secret = SecurityUtils.generate_totp_secret() totp = pyotp.TOTP(secret) valid_code = totp.now() assert SecurityUtils.verify_totp(secret, valid_code) is True def test_verify_invalid_totp_code(self): """Un código TOTP inválido debe retornar False.""" from app.core.security import SecurityUtils secret = SecurityUtils.generate_totp_secret() assert SecurityUtils.verify_totp(secret, "000000") is False def test_generate_totp_uri_contains_email(self): """El URI de TOTP debe contener el email del usuario.""" from app.core.security import SecurityUtils secret = SecurityUtils.generate_totp_secret() uri = SecurityUtils.generate_totp_uri(secret, "user@test.com") assert "user%40test.com" in uri or "user@test.com" in uri