""" Test unitari per app.core.security. """ import os import pytest # Override variabili d'ambiente per i test (prima di importare app) os.environ["ENCRYPTION_KEY"] = "a" * 64 os.environ["SECRET_KEY"] = "test-secret-key-for-unit-tests-only" os.environ["DATABASE_URL"] = "postgresql+asyncpg://test:test@localhost:5432/test" os.environ["DATABASE_URL_SYNC"] = "postgresql://test:test@localhost:5432/test" from app.core.security import ( hash_password, verify_password, create_access_token, create_refresh_token, decode_token, encrypt_credential, decrypt_credential, hash_token, ) class TestPasswordHashing: def test_hash_password_returns_bcrypt_hash(self): hashed = hash_password("MySecurePassword1!") assert hashed.startswith("$2b$") assert len(hashed) > 20 def test_verify_correct_password(self): password = "MySecurePassword1!" hashed = hash_password(password) assert verify_password(password, hashed) is True def test_verify_wrong_password(self): hashed = hash_password("CorrectPassword1!") assert verify_password("WrongPassword1!", hashed) is False def test_hash_is_different_each_time(self): """Bcrypt usa salt casuale: due hash dello stesso secret sono diversi.""" p = "SamePassword1!" h1 = hash_password(p) h2 = hash_password(p) assert h1 != h2 # Ma entrambi verificano correttamente assert verify_password(p, h1) assert verify_password(p, h2) class TestJWT: def test_create_and_decode_access_token(self): import uuid user_id = uuid.uuid4() tenant_id = uuid.uuid4() token = create_access_token( subject=user_id, tenant_id=tenant_id, role="admin", ) payload = decode_token(token) assert payload["sub"] == str(user_id) assert payload["tid"] == str(tenant_id) assert payload["role"] == "admin" assert payload["type"] == "access" def test_create_and_decode_refresh_token(self): import uuid user_id = uuid.uuid4() tenant_id = uuid.uuid4() token = create_refresh_token(subject=user_id, tenant_id=tenant_id) payload = decode_token(token) assert payload["sub"] == str(user_id) assert payload["type"] == "refresh" def test_expired_token_raises(self): from datetime import UTC, datetime, timedelta from jose import jwt from app.config import get_settings from jose import JWTError settings = get_settings() payload = { "sub": "user-id", "tid": "tenant-id", "type": "access", "exp": datetime.now(UTC) - timedelta(seconds=1), # già scaduto } token = jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm) with pytest.raises(JWTError): decode_token(token) def test_invalid_signature_raises(self): from jose import JWTError with pytest.raises(JWTError): decode_token("this.is.not.a.valid.token") class TestAESEncryption: def test_encrypt_decrypt_roundtrip(self): secret = "imap_password_super_secret_123!" encrypted = encrypt_credential(secret) decrypted = decrypt_credential(encrypted) assert decrypted == secret def test_encrypt_produces_different_output_each_time(self): """Nonce casuale garantisce che due cifrature dello stesso plaintext siano diverse.""" secret = "same_secret" enc1 = encrypt_credential(secret) enc2 = encrypt_credential(secret) assert enc1 != enc2 # Ma entrambi decifrano correttamente assert decrypt_credential(enc1) == secret assert decrypt_credential(enc2) == secret def test_decrypt_with_wrong_data_raises(self): with pytest.raises(ValueError): decrypt_credential("dGhpcyBpcyBub3QgdmFsaWQgYWVzIGRhdGE=") def test_encrypt_empty_string(self): encrypted = encrypt_credential("") decrypted = decrypt_credential(encrypted) assert decrypted == "" def test_encrypt_unicode_string(self): secret = "pàssword_con_àccenti_è_ù!" encrypted = encrypt_credential(secret) decrypted = decrypt_credential(encrypted) assert decrypted == secret class TestHashToken: def test_hash_is_deterministic(self): token = "my_refresh_token" assert hash_token(token) == hash_token(token) def test_different_tokens_different_hashes(self): assert hash_token("token1") != hash_token("token2") def test_hash_is_64_chars(self): """SHA-256 produce 32 bytes = 64 caratteri hex.""" assert len(hash_token("any_token")) == 64