feat: In-app AI Provider Configuration (#7650)

This commit is contained in:
Michael Genson
2026-05-23 11:13:10 -05:00
committed by GitHub
parent f6fe92b400
commit c3f87736d0
86 changed files with 3325 additions and 297 deletions

View File

@@ -0,0 +1,136 @@
"""
Integration tests for admin AI provider management across groups.
"""
from fastapi.testclient import TestClient
from mealie.schema.group.ai_providers import AIProviderCreate
from tests.utils import api_routes
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
# =======================================================================
# Permissions
def test_admin_ai_provider_routes_require_admin(api_client: TestClient, unique_user: TestUser, admin_user: TestUser):
"""Non-admin users cannot access admin AI provider routes."""
group_id = unique_user.group_id
response = api_client.post(
api_routes.admin_groups_group_id_ai_providers_providers(group_id),
json={"name": random_string(), "model": "gpt-4o", "apiKey": "key"},
headers=unique_user.token,
)
assert response.status_code == 403
# =======================================================================
# Provider CRUD
def test_admin_create_ai_provider_for_group(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
"""Admin can create an AI provider for any group."""
provider_name = random_string()
response = api_client.post(
api_routes.admin_groups_group_id_ai_providers_providers(unique_user.group_id),
json={"name": provider_name, "model": "gpt-4o", "apiKey": "admin-created-key"},
headers=admin_user.token,
)
assert response.status_code == 200
provider = response.json()
try:
assert provider["name"] == provider_name
assert provider["model"] == "gpt-4o"
assert "id" in provider
assert "apiKey" not in provider
finally:
unique_user.repos.group_ai_providers.delete(provider["id"])
def test_admin_get_ai_provider_for_group(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
"""Admin can retrieve an AI provider from any group."""
provider = unique_user.repos.group_ai_providers.create(
AIProviderCreate(name=random_string(), model="gpt-4o", api_key="secret")
)
try:
response = api_client.get(
api_routes.admin_groups_group_id_ai_providers_providers_provider_id(unique_user.group_id, provider.id),
headers=admin_user.token,
)
assert response.status_code == 200
data = response.json()
assert data["id"] == str(provider.id)
assert data["name"] == provider.name
finally:
unique_user.repos.group_ai_providers.delete(provider.id)
def test_admin_update_ai_provider_for_group(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
"""Admin can update an AI provider in any group."""
provider = unique_user.repos.group_ai_providers.create(
AIProviderCreate(name=random_string(), model="gpt-4o", api_key="original-key")
)
try:
new_name = random_string()
response = api_client.put(
api_routes.admin_groups_group_id_ai_providers_providers_provider_id(unique_user.group_id, provider.id),
json={"name": new_name, "model": "gpt-4-turbo", "apiKey": "updated-key"},
headers=admin_user.token,
)
assert response.status_code == 200
data = response.json()
assert data["name"] == new_name
assert data["model"] == "gpt-4-turbo"
finally:
unique_user.repos.group_ai_providers.delete(provider.id)
def test_admin_delete_ai_provider_for_group(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
"""Admin can delete an AI provider from any group."""
provider = unique_user.repos.group_ai_providers.create(
AIProviderCreate(name=random_string(), model="gpt-4o", api_key="to-delete")
)
response = api_client.delete(
api_routes.admin_groups_group_id_ai_providers_providers_provider_id(unique_user.group_id, provider.id),
headers=admin_user.token,
)
assert response.status_code == 200
# Confirm gone
response = api_client.get(
api_routes.admin_groups_group_id_ai_providers_providers_provider_id(unique_user.group_id, provider.id),
headers=admin_user.token,
)
assert response.status_code == 404
def test_admin_can_manage_providers_across_groups(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
"""Admin can create providers in a group they are not a member of."""
# Create a brand new group the admin doesn't belong to
group_name = random_string()
create_resp = api_client.post(api_routes.admin_groups, json={"name": group_name}, headers=admin_user.token)
assert create_resp.status_code == 201
foreign_group_id = create_resp.json()["id"]
try:
response = api_client.post(
api_routes.admin_groups_group_id_ai_providers_providers(foreign_group_id),
json={"name": random_string(), "model": "gpt-4o", "apiKey": "cross-group-key"},
headers=admin_user.token,
)
assert response.status_code == 200
provider_id = response.json()["id"]
# Settings should also be accessible
# Cleanup provider before deleting group
api_client.delete(
api_routes.admin_groups_group_id_ai_providers_providers_provider_id(foreign_group_id, provider_id),
headers=admin_user.token,
)
finally:
api_client.delete(api_routes.admin_groups_item_id(foreign_group_id), headers=admin_user.token)

View File

@@ -2,6 +2,7 @@ from fastapi.testclient import TestClient
from mealie.core.config import get_app_settings
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.group.ai_providers import AIProviderCreate, AIProviderSettingsUpdate
from mealie.schema.user.user import GroupInDB
from tests.utils import api_routes
from tests.utils.assertion_helpers import assert_ignore_keys
@@ -79,6 +80,100 @@ def test_admin_update_group(api_client: TestClient, admin_user: TestUser, unique
assert_ignore_keys(as_json["preferences"], update_payload["preferences"]) # type: ignore
def test_admin_update_group_name_only(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
"""Updating only the name leaves preferences and ai_provider_settings untouched."""
new_name = random_string()
response = api_client.put(
api_routes.admin_groups_item_id(unique_user.group_id),
json={"id": unique_user.group_id, "name": new_name},
headers=admin_user.token,
)
assert response.status_code == 200
assert response.json()["name"] == new_name
def test_admin_update_group_ai_provider_settings(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
"""Admin can update ai_provider_settings for a group via the PUT endpoint."""
provider = unique_user.repos.group_ai_providers.create(
AIProviderCreate(name=random_string(), model="gpt-4o", api_key="test-key")
)
try:
update_payload = {
"id": unique_user.group_id,
"name": unique_user.group_id, # name is required but unchanged
"aiProviderSettings": {
"defaultProviderId": str(provider.id),
"audioProviderId": str(provider.id),
"imageProviderId": str(provider.id),
},
}
response = api_client.put(
api_routes.admin_groups_item_id(unique_user.group_id),
json=update_payload,
headers=admin_user.token,
)
assert response.status_code == 200
settings = response.json()["aiProviderSettings"]
assert settings["defaultProviderId"] == str(provider.id)
assert settings["audioProviderId"] == str(provider.id)
assert settings["imageProviderId"] == str(provider.id)
finally:
# Clear provider references before deleting the provider
unique_user.repos.group_ai_provider_settings.update(
unique_user.repos.group_id,
AIProviderSettingsUpdate(default_provider_id=None, audio_provider_id=None, image_provider_id=None),
)
unique_user.repos.group_ai_providers.delete(provider.id)
def test_admin_update_group_ai_provider_settings_clear(
api_client: TestClient, admin_user: TestUser, unique_user: TestUser
):
"""Admin can clear ai_provider_settings provider IDs for a group."""
provider = unique_user.repos.group_ai_providers.create(
AIProviderCreate(name=random_string(), model="gpt-4o", api_key="test-key")
)
try:
# First set the providers
api_client.put(
api_routes.admin_groups_item_id(unique_user.group_id),
json={
"id": unique_user.group_id,
"name": unique_user.group_id,
"aiProviderSettings": {
"defaultProviderId": str(provider.id),
"audioProviderId": None,
"imageProviderId": None,
},
},
headers=admin_user.token,
)
# Now clear them
response = api_client.put(
api_routes.admin_groups_item_id(unique_user.group_id),
json={
"id": unique_user.group_id,
"name": unique_user.group_id,
"aiProviderSettings": {
"defaultProviderId": None,
"audioProviderId": None,
"imageProviderId": None,
},
},
headers=admin_user.token,
)
assert response.status_code == 200
settings = response.json()["aiProviderSettings"]
assert settings["defaultProviderId"] is None
assert settings["audioProviderId"] is None
assert settings["imageProviderId"] is None
finally:
unique_user.repos.group_ai_providers.delete(provider.id)
def test_admin_delete_group(unfiltered_database: AllRepositories, api_client: TestClient, admin_user: TestUser):
group = unfiltered_database.groups.create({"name": random_string()})
response = api_client.delete(api_routes.admin_groups_item_id(group.id), headers=admin_user.token)