mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 21:23:22 +08:00
refactor(api): reuse app definition queries for service API site
This commit is contained in:
parent
dfac3e524e
commit
6c5ff9be1a
@ -1,14 +1,14 @@
|
||||
from flask_restx import Resource
|
||||
from sqlalchemy import select
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.common.fields import Site as SiteResponse
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.wraps import validate_app_token
|
||||
from extensions.ext_database import db
|
||||
from models.account import TenantStatus
|
||||
from models.model import App, Site
|
||||
from extensions.ext_application_services import application_services
|
||||
from libs.helper import dump_response
|
||||
from models.model import App
|
||||
from services.app_definition_query_service import AppDefinitionUnavailableError
|
||||
|
||||
register_response_schema_models(service_api_ns, SiteResponse)
|
||||
|
||||
@ -49,13 +49,9 @@ class AppSiteApi(Resource):
|
||||
|
||||
Returns the site configuration for the application including theme, icons, and text.
|
||||
"""
|
||||
site = db.session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
|
||||
try:
|
||||
configuration = application_services().app_definitions.get_site_configuration(app_model.id)
|
||||
except AppDefinitionUnavailableError:
|
||||
raise Forbidden() from None
|
||||
|
||||
if not site:
|
||||
raise Forbidden()
|
||||
|
||||
assert app_model.tenant
|
||||
if app_model.tenant.status == TenantStatus.ARCHIVE:
|
||||
raise Forbidden()
|
||||
|
||||
return SiteResponse.model_validate(site).model_dump(mode="json")
|
||||
return dump_response(SiteResponse, configuration)
|
||||
|
||||
@ -11,13 +11,14 @@ from core.app.apps.agent_app.app_variable_projection import agent_app_variables_
|
||||
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
|
||||
from models.agent import AgentConfigSnapshot
|
||||
from models.agent_config_entities import AgentSoulConfig
|
||||
from models.model import App, AppMode, AppModelConfig, load_annotation_reply_config
|
||||
from models.model import App, AppMode, AppModelConfig, Site, load_annotation_reply_config
|
||||
from models.tools import ApiToolProvider
|
||||
from models.workflow import Workflow
|
||||
from services.app_definition_query_service import (
|
||||
AppDefinitionQuery,
|
||||
AppDefinitionSummary,
|
||||
AppParameterConfig,
|
||||
AppSiteConfiguration,
|
||||
AppToolIconSource,
|
||||
)
|
||||
|
||||
@ -145,6 +146,30 @@ class AppDefinitionQueryRepository(AppDefinitionQuery):
|
||||
author_name=app.author_name_with_session(session=session),
|
||||
)
|
||||
|
||||
@override
|
||||
def get_site_configuration(self, app_id: str) -> AppSiteConfiguration | None:
|
||||
with self._session_factory() as session:
|
||||
site = session.scalar(select(Site).where(Site.app_id == app_id).limit(1))
|
||||
if site is None:
|
||||
return None
|
||||
|
||||
return AppSiteConfiguration(
|
||||
title=site.title,
|
||||
chat_color_theme=site.chat_color_theme,
|
||||
chat_color_theme_inverted=site.chat_color_theme_inverted,
|
||||
icon_type=site.icon_type.value if site.icon_type is not None else None,
|
||||
icon=site.icon,
|
||||
icon_background=site.icon_background,
|
||||
description=site.description,
|
||||
copyright=site.copyright,
|
||||
privacy_policy=site.privacy_policy,
|
||||
input_placeholder=site.input_placeholder,
|
||||
custom_disclaimer=site.custom_disclaimer,
|
||||
default_language=site.default_language,
|
||||
show_workflow_steps=site.show_workflow_steps,
|
||||
use_icon_as_answer_icon=site.use_icon_as_answer_icon,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_tools(session: Session, app: App) -> list[dict[str, Any]]:
|
||||
if app.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||
|
||||
@ -28,6 +28,23 @@ class AppDefinitionSummary(NamedTuple):
|
||||
author_name: str | None
|
||||
|
||||
|
||||
class AppSiteConfiguration(NamedTuple):
|
||||
title: str
|
||||
chat_color_theme: str | None
|
||||
chat_color_theme_inverted: bool
|
||||
icon_type: str | None
|
||||
icon: str | None
|
||||
icon_background: str | None
|
||||
description: str | None
|
||||
copyright: str | None
|
||||
privacy_policy: str | None
|
||||
input_placeholder: str | None
|
||||
custom_disclaimer: str | None
|
||||
default_language: str
|
||||
show_workflow_steps: bool
|
||||
use_icon_as_answer_icon: bool
|
||||
|
||||
|
||||
class AppDefinitionQuery(Protocol):
|
||||
def get_published_parameter_config(
|
||||
self,
|
||||
@ -40,6 +57,8 @@ class AppDefinitionQuery(Protocol):
|
||||
|
||||
def get_summary(self, app_id: str) -> AppDefinitionSummary | None: ...
|
||||
|
||||
def get_site_configuration(self, app_id: str) -> AppSiteConfiguration | None: ...
|
||||
|
||||
|
||||
class AppDefinitionUnavailableError(ValueError):
|
||||
"""Raised when an app definition is unavailable."""
|
||||
@ -111,3 +130,9 @@ class AppDefinitionQueryService:
|
||||
if summary is None:
|
||||
raise AppDefinitionUnavailableError("App not found")
|
||||
return summary
|
||||
|
||||
def get_site_configuration(self, app_id: str) -> AppSiteConfiguration:
|
||||
configuration = self._definitions.get_site_configuration(app_id)
|
||||
if configuration is None:
|
||||
raise AppDefinitionUnavailableError("Site not found")
|
||||
return configuration
|
||||
|
||||
@ -1,106 +0,0 @@
|
||||
"""
|
||||
Testcontainers integration tests for Service API Site controller.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.service_api.app.site import AppSiteApi
|
||||
from models.account import Tenant, TenantStatus
|
||||
from models.model import App, AppMode, Site
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(flask_app_with_containers) -> Flask:
|
||||
return flask_app_with_containers
|
||||
|
||||
|
||||
from inspect import unwrap
|
||||
|
||||
|
||||
def _create_tenant(db_session: Session, *, status: TenantStatus = TenantStatus.NORMAL) -> Tenant:
|
||||
tenant = Tenant(name="service-api-site-tenant", status=status)
|
||||
db_session.add(tenant)
|
||||
db_session.commit()
|
||||
return tenant
|
||||
|
||||
|
||||
def _create_app(db_session: Session, tenant_id: str) -> App:
|
||||
app_model = App(
|
||||
tenant_id=tenant_id,
|
||||
mode=AppMode.CHAT,
|
||||
name="service-api-site-app",
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
status="normal",
|
||||
)
|
||||
db_session.add(app_model)
|
||||
db_session.commit()
|
||||
return app_model
|
||||
|
||||
|
||||
def _create_site(db_session: Session, app_id: str) -> Site:
|
||||
site = Site(
|
||||
app_id=app_id,
|
||||
title="Service API Site",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#ffffff",
|
||||
description="Service API test site",
|
||||
default_language="en-US",
|
||||
prompt_public=True,
|
||||
show_workflow_steps=True,
|
||||
customize_token_strategy="not_allow",
|
||||
use_icon_as_answer_icon=False,
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
)
|
||||
db_session.add(site)
|
||||
db_session.commit()
|
||||
return site
|
||||
|
||||
|
||||
class TestAppSiteApi:
|
||||
def test_get_site_success(self, app: Flask, db_session_with_containers: Session) -> None:
|
||||
tenant = _create_tenant(db_session_with_containers)
|
||||
app_model = _create_app(db_session_with_containers, tenant.id)
|
||||
_create_site(db_session_with_containers, app_model.id)
|
||||
|
||||
with app.test_request_context("/site", method="GET", headers={"Authorization": "Bearer test-token"}):
|
||||
api = AppSiteApi()
|
||||
response = unwrap(api.get)(api, app_model=app_model)
|
||||
|
||||
assert response["title"] == "Service API Site"
|
||||
assert response["icon"] == "robot"
|
||||
assert response["description"] == "Service API test site"
|
||||
|
||||
def test_get_site_not_found(self, app: Flask, db_session_with_containers: Session) -> None:
|
||||
tenant = _create_tenant(db_session_with_containers)
|
||||
app_model = _create_app(db_session_with_containers, tenant.id)
|
||||
|
||||
with app.test_request_context("/site", method="GET", headers={"Authorization": "Bearer test-token"}):
|
||||
api = AppSiteApi()
|
||||
with pytest.raises(Forbidden):
|
||||
unwrap(api.get)(api, app_model=app_model)
|
||||
|
||||
def test_get_site_tenant_archived(self, app: Flask, db_session_with_containers: Session) -> None:
|
||||
tenant = _create_tenant(db_session_with_containers)
|
||||
app_model = _create_app(db_session_with_containers, tenant.id)
|
||||
_create_site(db_session_with_containers, app_model.id)
|
||||
|
||||
archived_tenant = db_session_with_containers.get(Tenant, tenant.id)
|
||||
assert archived_tenant is not None
|
||||
archived_tenant.status = TenantStatus.ARCHIVE
|
||||
db_session_with_containers.commit()
|
||||
|
||||
app_model = db_session_with_containers.get(App, app_model.id)
|
||||
assert app_model is not None
|
||||
|
||||
with app.test_request_context("/site", method="GET", headers={"Authorization": "Bearer test-token"}):
|
||||
api = AppSiteApi()
|
||||
with pytest.raises(Forbidden):
|
||||
unwrap(api.get)(api, app_model=app_model)
|
||||
@ -16,7 +16,6 @@ project-excludes = [
|
||||
"controllers/console/test_apikey.py",
|
||||
"controllers/console/workspace/test_workspace_wraps.py",
|
||||
"controllers/service_api/dataset/test_dataset.py",
|
||||
"controllers/service_api/test_site.py",
|
||||
"controllers/web/test_conversation.py",
|
||||
"controllers/web/test_site.py",
|
||||
"controllers/web/test_wraps.py",
|
||||
|
||||
@ -17,8 +17,10 @@ from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
from werkzeug.exceptions import Forbidden, Unauthorized
|
||||
|
||||
from controllers.service_api.app import app as app_controller
|
||||
from controllers.service_api.app import site as site_controller
|
||||
from controllers.service_api.app.app import AppInfoApi, AppMetaApi, AppParameterApi
|
||||
from controllers.service_api.app.error import AgentNotPublishedError, AppUnavailableError
|
||||
from controllers.service_api.app.site import AppSiteApi
|
||||
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
|
||||
from models.base import TypeBase
|
||||
@ -27,6 +29,7 @@ from services.app_definition_query_service import (
|
||||
AppDefinitionNotPublishedError,
|
||||
AppDefinitionSummary,
|
||||
AppDefinitionUnavailableError,
|
||||
AppSiteConfiguration,
|
||||
)
|
||||
|
||||
|
||||
@ -290,6 +293,61 @@ def test_get_info_maps_unavailable_app(
|
||||
AppInfoApi().get()
|
||||
|
||||
|
||||
def test_get_site_configuration_queries_authenticated_app(
|
||||
flask_app: Flask,
|
||||
authenticated_controller: AppDatabase,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
app_definitions = Mock()
|
||||
app_definitions.get_site_configuration.return_value = AppSiteConfiguration(
|
||||
title="Test Site",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#ffffff",
|
||||
description="A test site",
|
||||
copyright=None,
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask anything",
|
||||
custom_disclaimer=None,
|
||||
default_language="en-US",
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
site_controller,
|
||||
"application_services",
|
||||
Mock(return_value=SimpleNamespace(app_definitions=app_definitions)),
|
||||
)
|
||||
|
||||
with flask_app.test_request_context("/site", headers={"Authorization": "Bearer token"}):
|
||||
response = AppSiteApi().get()
|
||||
|
||||
app_definitions.get_site_configuration.assert_called_once_with(authenticated_controller.app_id)
|
||||
assert response["title"] == "Test Site"
|
||||
assert response["icon"] == "robot"
|
||||
assert response["icon_url"] is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("authenticated_controller")
|
||||
def test_get_site_configuration_maps_missing_site_to_forbidden(
|
||||
flask_app: Flask,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
app_definitions = Mock()
|
||||
app_definitions.get_site_configuration.side_effect = AppDefinitionUnavailableError("Site not found")
|
||||
monkeypatch.setattr(
|
||||
site_controller,
|
||||
"application_services",
|
||||
Mock(return_value=SimpleNamespace(app_definitions=app_definitions)),
|
||||
)
|
||||
|
||||
with flask_app.test_request_context("/site", headers={"Authorization": "Bearer token"}):
|
||||
with pytest.raises(Forbidden):
|
||||
AppSiteApi().get()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", ["missing", "disabled", "archived", "ownerless"])
|
||||
def test_authentication_rejects_empty_or_invisible_database_state(
|
||||
flask_app: Flask,
|
||||
|
||||
@ -5,12 +5,17 @@ from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from core.tools.entities.tool_entities import ApiProviderSchemaType
|
||||
from models.account import Account
|
||||
from models.enums import TagType
|
||||
from models.model import App, AppMode, AppModelConfig, Tag, TagBinding
|
||||
from models.enums import CustomizeTokenStrategy, TagType
|
||||
from models.model import App, AppMode, AppModelConfig, IconType, Site, Tag, TagBinding
|
||||
from models.tools import ApiToolProvider
|
||||
from models.workflow import Workflow, WorkflowKind, WorkflowType
|
||||
from repositories.app_definition_query_repository import AppDefinitionQueryRepository
|
||||
from services.app_definition_query_service import AppDefinitionSummary, AppParameterConfig, AppToolIconSource
|
||||
from services.app_definition_query_service import (
|
||||
AppDefinitionSummary,
|
||||
AppParameterConfig,
|
||||
AppSiteConfiguration,
|
||||
AppToolIconSource,
|
||||
)
|
||||
|
||||
_APP_ID = "11111111-1111-1111-1111-111111111111"
|
||||
_TENANT_ID = "22222222-2222-2222-2222-222222222222"
|
||||
@ -290,6 +295,57 @@ def test_get_summary_returns_only_tenant_scoped_app_tags(
|
||||
assert result.author_name is None
|
||||
|
||||
|
||||
def test_get_site_configuration_returns_none_for_missing_site(
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
) -> None:
|
||||
repository = AppDefinitionQueryRepository(session_factory=sqlite_session_factory)
|
||||
|
||||
assert repository.get_site_configuration(_APP_ID) is None
|
||||
|
||||
|
||||
def test_get_site_configuration_maps_site_fields(sqlite_session_factory: sessionmaker[Session]) -> None:
|
||||
with sqlite_session_factory.begin() as session:
|
||||
site = Site(
|
||||
app_id=_APP_ID,
|
||||
title="Test Site",
|
||||
icon_type=IconType.IMAGE,
|
||||
icon="11111111-1111-4111-8111-111111111111",
|
||||
icon_background="#ffffff",
|
||||
description="A test site",
|
||||
default_language="en-US",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=True,
|
||||
copyright="Copyright",
|
||||
privacy_policy="Privacy",
|
||||
input_placeholder="Ask anything",
|
||||
show_workflow_steps=False,
|
||||
use_icon_as_answer_icon=True,
|
||||
customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW,
|
||||
prompt_public=True,
|
||||
)
|
||||
site.custom_disclaimer = "Disclaimer"
|
||||
session.add(site)
|
||||
|
||||
result = AppDefinitionQueryRepository(session_factory=sqlite_session_factory).get_site_configuration(_APP_ID)
|
||||
|
||||
assert result == AppSiteConfiguration(
|
||||
title="Test Site",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=True,
|
||||
icon_type=IconType.IMAGE.value,
|
||||
icon="11111111-1111-4111-8111-111111111111",
|
||||
icon_background="#ffffff",
|
||||
description="A test site",
|
||||
copyright="Copyright",
|
||||
privacy_policy="Privacy",
|
||||
input_placeholder="Ask anything",
|
||||
custom_disclaimer="Disclaimer",
|
||||
default_language="en-US",
|
||||
show_workflow_steps=False,
|
||||
use_icon_as_answer_icon=True,
|
||||
)
|
||||
|
||||
|
||||
def _tool(provider_type: str, provider_id: str, tool_name: str) -> dict[str, object]:
|
||||
return {
|
||||
"provider_type": provider_type,
|
||||
|
||||
@ -145,3 +145,11 @@ def test_get_summary_rejects_missing_app() -> None:
|
||||
|
||||
with pytest.raises(AppDefinitionUnavailableError, match="App not found"):
|
||||
service.get_summary("missing")
|
||||
|
||||
|
||||
def test_get_site_configuration_rejects_missing_site() -> None:
|
||||
service, definitions = _service()
|
||||
definitions.get_site_configuration.return_value = None
|
||||
|
||||
with pytest.raises(AppDefinitionUnavailableError, match="Site not found"):
|
||||
service.get_site_configuration("app-1")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user