Files
kite/backend/tests/test_ai_config.py
curo1305 95c386f764 feat(07-03): classifier wired to load_provider_config + ai_config stub removed — D-04/D-06
- Remove _ProviderConfigStub from services/ai_config.py (replaced by real ProviderConfig)
- Add module-level import: from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS
- load_provider_config() now returns ProviderConfig (no lazy import inside function body)
- classifier.py: replace inline _settings dict with load_provider_config(session) call (D-06)
- Per-user override path builds ProviderConfig from PROVIDER_DEFAULTS (no api_key — T-07-06)
- Fallback to app_settings defaults when load_provider_config returns None (D-15)
- Truncation delegated to provider._truncate() — no more text slices in classifier (D-12/D-13)
- Promote test_api_key_encrypt_decrypt to passing (round-trip + domain salt isolation)
- Update test_classifier.py: test_per_user_provider + test_default_provider_fallback use ProviderConfig assertions
2026-06-04 19:14:48 +02:00

101 lines
4.0 KiB
Python

"""
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