from types import SimpleNamespace import pytest from app.api.v1 import settings as settings_api from app.api.v1.settings import ( AIProviderIntegrationUpdate, OCRIntegrationUpdate, _build_ai_provider_payload, _build_ocr_payload, _mask_secret, _normalize_ai_provider_payload, _normalize_ocr_payload, _resolve_provider_api_key, get_runtime_ai_provider_config, ) @pytest.fixture(autouse=True) def isolated_ai_provider_env_file(monkeypatch, tmp_path): env_file = tmp_path / ".env" monkeypatch.setattr(settings_api, "AI_PROVIDER_ENV_FILE", env_file) return env_file def test_legacy_ai_provider_payload_maps_to_provider_config(): payload = _normalize_ai_provider_payload( { "provider": "openai", "provider_api": "openai-completions", "base_url": "https://api.openai.example/v1", "model": "gpt-test", "api_key": "old-openai-key", "max_tokens": 2048, "anthropic_version": "2023-06-01", } ) assert payload["default_provider"] == "openai" assert payload["providers"]["openai"]["api_key"] == "old-openai-key" assert payload["providers"]["openai"]["model"] == "gpt-test" assert payload["providers"]["openai"]["base_url"] == "https://api.openai.example/v1" def test_provider_key_prefers_specific_env_file_key(isolated_ai_provider_env_file): isolated_ai_provider_env_file.write_text( "OPENAI_API_KEY=openai-env-file-key\nAI_API_KEY=generic-env-file-key\n", encoding="utf-8", ) value, source = _resolve_provider_api_key("openai", {"api_key": ""}) assert value == "openai-env-file-key" assert source == "env_file" def test_provider_key_falls_back_to_generic_ai_api_key(isolated_ai_provider_env_file): isolated_ai_provider_env_file.write_text( "AI_API_KEY=generic-env-file-key\n", encoding="utf-8", ) value, source = _resolve_provider_api_key("openai", {"api_key": ""}) assert value == "generic-env-file-key" assert source == "env_file" def test_mask_secret_without_prefix_is_fully_masked(): assert _mask_secret("plainsecret")["preview"] == "***********" assert _mask_secret("sk-prefixed")["preview"] == "sk-********" def test_build_payload_updates_only_selected_provider_key(): current = { "ai_provider": { "default_provider": "openai", "providers": { "openai": { "provider": "openai", "provider_api": "openai-completions", "base_url": "https://api.openai.com/v1", "model": "gpt-old", "api_key": "openai-old-key", "max_tokens": 4096, "anthropic_version": "2023-06-01", }, "minimax": { "provider": "minimax", "api_key": "minimax-old-key", }, }, } } update = AIProviderIntegrationUpdate( provider="openai", provider_api="openai-completions", base_url="https://api.openai.com/v1", model="gpt-new", api_key="openai-new-key", max_tokens=8192, ) payload = _build_ai_provider_payload(current, update) assert payload["default_provider"] == "openai" assert payload["providers"]["openai"]["api_key"] == "openai-new-key" assert payload["providers"]["openai"]["model"] == "gpt-new" assert payload["providers"]["minimax"]["api_key"] == "minimax-old-key" def test_build_payload_keeps_saved_key_when_preview_submitted(): current = { "ai_provider": { "providers": { "openai": { "provider": "openai", "api_key": "sk-old-secret", }, }, } } update = AIProviderIntegrationUpdate( provider="openai", provider_api="openai-completions", base_url="https://api.openai.com/v1", model="gpt-test", api_key="sk-*********", ) payload = _build_ai_provider_payload(current, update) assert payload["providers"]["openai"]["api_key"] == "sk-old-secret" def test_normalize_ocr_payload_adds_defaults(): payload = _normalize_ocr_payload({}) assert payload["enabled"] is False assert payload["provider"] == "paddleocr" assert payload["languages"] == ["zh", "en"] assert payload["output_format"] == "markdown" def test_build_ocr_payload_keeps_saved_key_when_preview_submitted(): current = { "ocr": { "enabled": True, "provider": "custom", "base_url": "http://localhost:8020", "api_key": "ocr-old-secret", } } update = OCRIntegrationUpdate( enabled=True, provider="custom", base_url="http://localhost:8020", api_key="**************", model="ocr-model", languages=["zh", "en"], timeout_seconds=45, max_file_size_mb=50, output_format="json", ) payload = _build_ocr_payload(current, update) assert payload["api_key"] == "ocr-old-secret" assert payload["model"] == "ocr-model" assert payload["output_format"] == "json" @pytest.mark.asyncio async def test_runtime_config_uses_default_provider_specific_key(monkeypatch): record = SimpleNamespace( payload={ "ai_provider": { "default_provider": "minimax", "providers": { "openai": { "provider": "openai", "api_key": "openai-key", "provider_api": "openai-completions", "base_url": "https://api.openai.com/v1", "model": "gpt-test", }, "minimax": { "provider": "minimax", "api_key": "minimax-key", "provider_api": "anthropic-messages", "base_url": "https://api.minimaxi.com/anthropic", "model": "MiniMax-test", }, }, } } ) async def fake_get_setting_record(_db, category): assert category == "external_integrations" return record monkeypatch.setattr(settings_api, "get_setting_record", fake_get_setting_record) runtime_config = await get_runtime_ai_provider_config(object()) assert runtime_config["llm_config"]["provider"] == "minimax" assert runtime_config["llm_config"]["api_key"] == "minimax-key" assert runtime_config["llm_config"]["model"] == "MiniMax-test"