""" Tests for Phase 7 AI config service. Covers: - D-04: load_provider_config() reads from system_settings table - D-05: API key HKDF encryption round-trip Plan 07-03 promotes: test_api_key_encrypt_decrypt, test_load_provider_config. """ import os import pytest # --------------------------------------------------------------------------- # test_api_key_encrypt_decrypt — D-05 HKDF encryption round-trip # --------------------------------------------------------------------------- def test_api_key_encrypt_decrypt(): """encrypt_api_key + decrypt_api_key round-trip returns original plaintext. Also verifies domain salt isolation: different provider_ids produce different ciphertexts from the same master key and plaintext (T-07-02). """ from services.ai_config import encrypt_api_key, decrypt_api_key # Use a 32-byte master key (CLOUD_CREDS_KEY is Base64-encoded in production, # but the helpers accept raw bytes — use ASCII bytes for simplicity here) master_key = b"a" * 32 # 32 bytes deterministic test key # Round-trip: encrypt then decrypt must return original plaintext plaintext = "sk-test-api-key-abc123" ciphertext = encrypt_api_key(master_key, "openai", plaintext) recovered = decrypt_api_key(master_key, "openai", ciphertext) assert recovered == plaintext, "Decrypted value must match original plaintext" # Domain salt isolation: same key + same plaintext, different provider_id → different ciphertext ciphertext_anthropic = encrypt_api_key(master_key, "anthropic", plaintext) assert ciphertext != ciphertext_anthropic, ( "Different provider_ids must produce different ciphertexts " "(HKDF salt isolation — T-07-02)" ) # Cross-provider decrypt must fail (Fernet raises InvalidToken) from cryptography.fernet import InvalidToken with pytest.raises(InvalidToken): decrypt_api_key(master_key, "anthropic", ciphertext) # openai key, anthropic provider_id # --------------------------------------------------------------------------- # test_load_provider_config — D-04 DB integration (skipped without PostgreSQL) # --------------------------------------------------------------------------- @pytest.mark.skipif( not os.getenv("INTEGRATION"), reason="needs PostgreSQL — set INTEGRATION=1 to run", ) @pytest.mark.asyncio async def test_load_provider_config(db_session): """load_provider_config() reads the active row from system_settings and decrypts the api_key. Requires a live PostgreSQL session (db_session fixture from conftest.py). Run with: INTEGRATION=1 pytest tests/test_ai_config.py::test_load_provider_config """ pytest.importorskip("psycopg") from services.ai_config import encrypt_api_key, load_provider_config from db.models import SystemSettings import uuid master_key = b"b" * 32 # deterministic test key test_api_key = "sk-integration-test-key" provider_id = f"test-provider-{uuid.uuid4().hex[:8]}" # Encrypt the API key api_key_enc = encrypt_api_key(master_key, provider_id, test_api_key) # Insert a SystemSettings row with is_active=True row = SystemSettings( provider_id=provider_id, api_key_enc=api_key_enc, base_url="https://api.example.com/v1", model_name="test-model", context_chars=64000, is_active=True, ) db_session.add(row) await db_session.flush() # Patch settings.cloud_creds_key to match our test master key from unittest.mock import patch with patch("services.ai_config.settings") as mock_settings: mock_settings.cloud_creds_key = master_key.decode("utf-8") result = await load_provider_config(db_session) assert result is not None, "load_provider_config must return a ProviderConfig" assert result.provider_id == provider_id assert result.api_key == test_api_key, "api_key must be decrypted correctly" assert result.base_url == "https://api.example.com/v1" assert result.model == "test-model" assert result.context_chars == 64000