Files
dify/api/tests/unit_tests/controllers/openapi/test__app_response.py
T

151 lines
6.0 KiB
Python

from __future__ import annotations
from collections.abc import Callable
from datetime import datetime
from typing import Protocol, cast
import pytest
from flask import Flask
from controllers.openapi._app_response import _EMPTY_PARAMETERS, build_app_describe_response
from controllers.openapi._input_schema import EMPTY_INPUT_SCHEMA
from controllers.openapi._models import AppDescribeQuery, AppDescribeResponse
from controllers.openapi.apps import AppDescribeApi, settings_hints
from controllers.openapi.auth.requirements import CheckWebAppAuthEnterprise
from machinery.context import AppRequestContext
from models.model import AppMode
from services.app.query_service import AppDescription
from services.app_definition_query_service import AppParameterConfig
from services.entities.app_entities import SUPPORTED_APP_TYPES, AppSummary, SupportedAppType
from tests.unit_tests.controllers.conftest import ControllerTestServices
class Endpoint[T](Protocol):
__handler__: Callable[..., T]
def description(config: AppParameterConfig | None, mode: str = AppMode.CHAT) -> AppDescription:
return AppDescription(
AppSummary(
"app-id", "tenant-id", "Demo", "d", cast(AppMode, mode), "normal", datetime(2026, 1, 1), "maintainer"
),
True,
config,
)
def test_full_description_projects_materialized_data_without_request_or_session() -> None:
form = [{"text-input": {"variable": "industry", "label": "Industry", "required": True}}]
response = build_app_describe_response(description(AppParameterConfig({}, form)), None)
assert response.info is not None
assert response.info.model_dump() == {
"id": "app-id",
"name": "Demo",
"description": "d",
"mode": "chat",
"updated_at": "2026-01-01T00:00:00",
"service_api_enabled": True,
"is_agent": False,
}
assert response.parameters is not None
assert response.parameters["user_input_form"] == form
assert response.input_schema is not None
assert response.input_schema["properties"]["inputs"]["required"] == ["industry"]
@pytest.mark.parametrize("fields", [{"info"}, {"parameters"}, {"input_schema"}, set()])
def test_projection_honors_requested_fields_and_missing_config_fallbacks(fields: set[str]) -> None:
response = build_app_describe_response(description(None), fields)
assert (response.info is not None) == ("info" in fields)
assert response.parameters == (_EMPTY_PARAMETERS if "parameters" in fields else None)
assert response.input_schema == (EMPTY_INPUT_SCHEMA if "input_schema" in fields else None)
def test_empty_parameters_preserve_public_response_contract() -> None:
assert _EMPTY_PARAMETERS == {
"opening_statement": None,
"suggested_questions": [],
"user_input_form": [],
"file_upload": None,
"system_parameters": {},
}
def test_settings_hints_name_the_mode_ops(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(CheckWebAppAuthEnterprise, "available", staticmethod(lambda: True))
ops = [hint.op for hint in settings_hints("app-1", AppMode.WORKFLOW)]
assert ops == [
"describe.app_info.workflow",
"describe.webapp.workflow",
"describe.service_api",
"describe.webapp_access",
]
agent_ops = [hint.op for hint in settings_hints("app-1", AppMode.AGENT)]
assert agent_ops == [
"describe.app_info.agent",
"describe.webapp.agent",
"describe.service_api.agent",
"describe.webapp_access.agent",
]
assert all(hint.input == {"app_id": "app-1"} for hint in settings_hints("app-1", AppMode.CHAT))
def test_settings_hints_skip_web_app_access_without_web_app_auth(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(CheckWebAppAuthEnterprise, "available", staticmethod(lambda: False))
ops = [hint.op for hint in settings_hints("app-1", AppMode.WORKFLOW)]
assert "describe.webapp_access" not in ops
assert ops
def test_shared_builder_adds_no_hints() -> None:
"""The builder also serves describe.console_app.external, whose callers can't use account-only settings ops."""
assert build_app_describe_response(description(None), None).hints == []
def _describe(
app: Flask, services: ControllerTestServices, monkeypatch: pytest.MonkeyPatch, mode: str
) -> AppDescribeResponse:
monkeypatch.setattr(services.apps.discovery, "describe", lambda *_args: description(None, mode))
api = AppDescribeApi()
with app.test_request_context():
return cast(Endpoint[AppDescribeResponse], api.get).__handler__(
api, AppRequestContext(tenant_id="tenant-id", app_id="app-id"), "app-id", query=AppDescribeQuery()
)
def test_account_describe_adds_settings_hints(
app: Flask, app_query_services: ControllerTestServices, monkeypatch: pytest.MonkeyPatch
) -> None:
resp = _describe(app, app_query_services, monkeypatch, AppMode.CHAT)
assert [hint.op for hint in resp.hints] == [hint.op for hint in settings_hints("app-id", AppMode.CHAT)]
assert resp.hints
def test_account_describe_unknown_mode_has_no_hints(
app: Flask, app_query_services: ControllerTestServices, monkeypatch: pytest.MonkeyPatch
) -> None:
resp = _describe(app, app_query_services, monkeypatch, "mode-from-a-newer-version")
assert resp.hints == []
assert resp.info is not None
"""Unit tests for SupportedAppType — the listable subset of AppMode that the
openapi `app` face (`get app`) exposes and the CLI `--mode` whitelist derives from.
"""
def test_supported_app_type_is_the_listable_subset_of_app_mode() -> None:
"""SupportedAppType (and the derived SUPPORTED_APP_TYPES tuple) is exactly the
curated, listable subset of AppMode; non-app/runtime modes stay out."""
assert {t.value for t in SupportedAppType} == {
"completion",
"chat",
"advanced-chat",
"workflow",
"agent-chat",
}
assert set(SUPPORTED_APP_TYPES) <= set(AppMode)
assert AppMode.AGENT not in SUPPORTED_APP_TYPES
assert AppMode.RAG_PIPELINE not in SUPPORTED_APP_TYPES
assert AppMode.CHANNEL not in SUPPORTED_APP_TYPES