mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-05-26 11:40:27 -04:00
feat: In-app AI Provider Configuration (#7650)
This commit is contained in:
206
tests/unit_tests/schema_tests/test_ai_providers.py
Normal file
206
tests/unit_tests/schema_tests/test_ai_providers.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mealie.schema.group.ai_providers import (
|
||||
AIProviderCreate,
|
||||
AIProviderSettingsOut,
|
||||
AIProviderSummary,
|
||||
)
|
||||
|
||||
|
||||
class AIProviderCreateTests:
|
||||
def test_valid_create(self):
|
||||
provider = AIProviderCreate(name="test", api_key="key", model="gpt-4o")
|
||||
assert provider.name == "test"
|
||||
assert provider.model == "gpt-4o"
|
||||
assert provider.timeout == 300
|
||||
assert provider.base_url is None
|
||||
|
||||
@pytest.mark.parametrize("field", ["name", "api_key", "model"])
|
||||
def test_empty_field_raises(self, field: str):
|
||||
data: dict = {"name": "test", "api_key": "key", "model": "gpt-4o", field: ""}
|
||||
with pytest.raises(ValidationError):
|
||||
AIProviderCreate(**data)
|
||||
|
||||
@pytest.mark.parametrize("timeout", [-1, -100])
|
||||
def test_negative_timeout_raises(self, timeout: int):
|
||||
with pytest.raises(ValidationError):
|
||||
AIProviderCreate(name="test", api_key="key", model="gpt-4o", timeout=timeout)
|
||||
|
||||
def test_zero_timeout_is_valid(self):
|
||||
provider = AIProviderCreate(name="test", api_key="key", model="gpt-4o", timeout=0)
|
||||
assert provider.timeout == 0
|
||||
|
||||
@pytest.mark.parametrize("base_url", ["", None])
|
||||
def test_base_url_empty_becomes_none(self, base_url: str | None):
|
||||
provider = AIProviderCreate(name="test", api_key="key", model="gpt-4o", base_url=base_url)
|
||||
assert provider.base_url is None
|
||||
|
||||
def test_api_key_excluded_from_serialization(self):
|
||||
provider = AIProviderCreate(name="test", api_key="secret", model="gpt-4o")
|
||||
dumped = provider.model_dump()
|
||||
assert "api_key" not in dumped
|
||||
|
||||
def test_api_key_excluded_from_json(self):
|
||||
provider = AIProviderCreate(name="test", api_key="secret", model="gpt-4o")
|
||||
json_str = provider.model_dump_json()
|
||||
assert "api_key" not in json_str
|
||||
assert "secret" not in json_str
|
||||
|
||||
|
||||
class AIProviderSettingsOutTests:
|
||||
def _make_settings(
|
||||
self,
|
||||
*,
|
||||
default_provider_id=None,
|
||||
audio_provider_id=None,
|
||||
image_provider_id=None,
|
||||
providers=None,
|
||||
) -> AIProviderSettingsOut:
|
||||
if providers is None:
|
||||
providers = []
|
||||
return AIProviderSettingsOut(
|
||||
default_provider_id=default_provider_id,
|
||||
audio_provider_id=audio_provider_id,
|
||||
image_provider_id=image_provider_id,
|
||||
providers=providers,
|
||||
)
|
||||
|
||||
# --- ai_enabled ---
|
||||
|
||||
def test_ai_enabled_false_when_no_default(self):
|
||||
s = self._make_settings()
|
||||
assert not s.ai_enabled
|
||||
|
||||
def test_ai_enabled_true_when_default_set(self):
|
||||
pid = uuid4()
|
||||
s = self._make_settings(default_provider_id=pid, providers=[AIProviderSummary(id=pid, name="p")])
|
||||
assert s.ai_enabled
|
||||
|
||||
# --- audio_provider_enabled ---
|
||||
|
||||
def test_audio_provider_disabled_when_no_default(self):
|
||||
audio_id = uuid4()
|
||||
s = self._make_settings(
|
||||
audio_provider_id=audio_id,
|
||||
providers=[AIProviderSummary(id=audio_id, name="audio")],
|
||||
)
|
||||
# audio_provider_id is valid, but validate_providers sets audio_provider_id to None
|
||||
# because without default_provider_id, it would be fine; let's test audio_provider_enabled
|
||||
# which requires ai_enabled to be True
|
||||
assert not s.ai_enabled
|
||||
assert not s.audio_provider_enabled
|
||||
|
||||
def test_audio_provider_disabled_when_only_default_set(self):
|
||||
pid = uuid4()
|
||||
s = self._make_settings(default_provider_id=pid, providers=[AIProviderSummary(id=pid, name="p")])
|
||||
assert s.ai_enabled
|
||||
assert not s.audio_provider_enabled
|
||||
|
||||
def test_audio_provider_enabled_when_both_set(self):
|
||||
pid = uuid4()
|
||||
audio_id = uuid4()
|
||||
s = self._make_settings(
|
||||
default_provider_id=pid,
|
||||
audio_provider_id=audio_id,
|
||||
providers=[AIProviderSummary(id=pid, name="p"), AIProviderSummary(id=audio_id, name="audio")],
|
||||
)
|
||||
assert s.ai_enabled
|
||||
assert s.audio_provider_enabled
|
||||
|
||||
# --- image_provider_enabled ---
|
||||
|
||||
def test_image_provider_disabled_when_no_default(self):
|
||||
image_id = uuid4()
|
||||
s = self._make_settings(
|
||||
image_provider_id=image_id,
|
||||
providers=[AIProviderSummary(id=image_id, name="img")],
|
||||
)
|
||||
assert not s.ai_enabled
|
||||
assert not s.image_provider_enabled
|
||||
|
||||
def test_image_provider_disabled_when_only_default_set(self):
|
||||
pid = uuid4()
|
||||
s = self._make_settings(default_provider_id=pid, providers=[AIProviderSummary(id=pid, name="p")])
|
||||
assert s.ai_enabled
|
||||
assert not s.image_provider_enabled
|
||||
|
||||
def test_image_provider_enabled_when_both_set(self):
|
||||
pid = uuid4()
|
||||
image_id = uuid4()
|
||||
s = self._make_settings(
|
||||
default_provider_id=pid,
|
||||
image_provider_id=image_id,
|
||||
providers=[AIProviderSummary(id=pid, name="p"), AIProviderSummary(id=image_id, name="img")],
|
||||
)
|
||||
assert s.ai_enabled
|
||||
assert s.image_provider_enabled
|
||||
|
||||
# --- validate_providers model validator ---
|
||||
|
||||
def test_validate_providers_strips_unknown_default(self):
|
||||
s = self._make_settings(default_provider_id=uuid4(), providers=[])
|
||||
assert s.default_provider_id is None
|
||||
assert not s.ai_enabled
|
||||
|
||||
def test_validate_providers_strips_unknown_audio(self):
|
||||
pid = uuid4()
|
||||
providers = [AIProviderSummary(id=pid, name="p")]
|
||||
s = self._make_settings(default_provider_id=pid, audio_provider_id=uuid4(), providers=providers)
|
||||
assert s.default_provider_id == pid
|
||||
assert s.audio_provider_id is None
|
||||
|
||||
def test_validate_providers_strips_unknown_image(self):
|
||||
pid = uuid4()
|
||||
providers = [AIProviderSummary(id=pid, name="p")]
|
||||
s = self._make_settings(default_provider_id=pid, image_provider_id=uuid4(), providers=providers)
|
||||
assert s.default_provider_id == pid
|
||||
assert s.image_provider_id is None
|
||||
|
||||
def test_validate_providers_keeps_valid_ids(self):
|
||||
pid = uuid4()
|
||||
audio_id = uuid4()
|
||||
image_id = uuid4()
|
||||
providers = [
|
||||
AIProviderSummary(id=pid, name="p"),
|
||||
AIProviderSummary(id=audio_id, name="audio"),
|
||||
AIProviderSummary(id=image_id, name="img"),
|
||||
]
|
||||
s = self._make_settings(
|
||||
default_provider_id=pid,
|
||||
audio_provider_id=audio_id,
|
||||
image_provider_id=image_id,
|
||||
providers=providers,
|
||||
)
|
||||
assert s.default_provider_id == pid
|
||||
assert s.audio_provider_id == audio_id
|
||||
assert s.image_provider_id == image_id
|
||||
|
||||
def test_validate_providers_strips_all_if_empty_list(self):
|
||||
pid = uuid4()
|
||||
s = self._make_settings(
|
||||
default_provider_id=pid,
|
||||
audio_provider_id=uuid4(),
|
||||
image_provider_id=uuid4(),
|
||||
providers=[],
|
||||
)
|
||||
assert s.default_provider_id is None
|
||||
assert s.audio_provider_id is None
|
||||
assert s.image_provider_id is None
|
||||
|
||||
def test_validate_providers_partial_strip(self):
|
||||
"""Only the IDs pointing to missing providers are stripped."""
|
||||
pid = uuid4()
|
||||
audio_id = uuid4()
|
||||
providers = [AIProviderSummary(id=pid, name="p"), AIProviderSummary(id=audio_id, name="audio")]
|
||||
s = self._make_settings(
|
||||
default_provider_id=pid,
|
||||
audio_provider_id=audio_id,
|
||||
image_provider_id=uuid4(), # not in list → stripped
|
||||
providers=providers,
|
||||
)
|
||||
assert s.default_provider_id == pid
|
||||
assert s.audio_provider_id == audio_id
|
||||
assert s.image_provider_id is None
|
||||
Reference in New Issue
Block a user