mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 13:51:25 +08:00
75 lines
3.2 KiB
Python
75 lines
3.2 KiB
Python
"""Dispatch identity remains bound to the caller across installation changes."""
|
|
|
|
import json
|
|
from typing import Never
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from core.plugin.impl import base as plugin_client_module
|
|
from core.plugin.impl.base import BasePluginClient
|
|
from core.plugin.plugin_service import PluginService
|
|
from services.plugin.dispatch_identity import PLUGIN_IDENTIFIER_HEADER, prepare_dispatch_identity
|
|
|
|
PLUGIN_ID = "langgenius/example"
|
|
IDENTIFIER = f"{PLUGIN_ID}:1.0.0@{'a' * 32}"
|
|
|
|
|
|
def _unexpected_configuration_access(*_args: object, **_kwargs: object) -> Never:
|
|
raise AssertionError("Dispatch must not discover installations or invalidate configuration")
|
|
|
|
|
|
@pytest.mark.parametrize("stream", [False, True])
|
|
def test_dispatch_keeps_declared_version_without_refetch_or_invalidation(
|
|
monkeypatch: pytest.MonkeyPatch, stream: bool
|
|
) -> None:
|
|
monkeypatch.setattr(PluginService, "list_installations_from_ids", _unexpected_configuration_access)
|
|
monkeypatch.setattr(PluginService, "invalidate_plugin_model_providers_cache", _unexpected_configuration_access)
|
|
requests: list[httpx.Request] = []
|
|
|
|
def handle(request: httpx.Request) -> httpx.Response:
|
|
requests.append(request)
|
|
return httpx.Response(200, text='data: {"code": 0, "message": "", "data": true}\n')
|
|
|
|
with httpx.Client(transport=httpx.MockTransport(handle)) as http_client:
|
|
monkeypatch.setattr(plugin_client_module, "_httpx_client", http_client)
|
|
client = BasePluginClient(plugin_unique_identifier=IDENTIFIER)
|
|
path = "plugin/tenant-a/dispatch/tool/invoke"
|
|
headers = {"X-Plugin-ID": PLUGIN_ID, "Content-Type": "application/json"}
|
|
data = {"data": {"credentials": {"api_key": "secret"}}}
|
|
if stream:
|
|
assert len(list(client._stream_request("POST", path, headers=headers, data=data))) == 1
|
|
else:
|
|
assert client._request("POST", path, headers=headers, data=data).status_code == 200
|
|
|
|
assert len(requests) == 1
|
|
assert requests[0].headers["X-Plugin-ID"] == PLUGIN_ID
|
|
assert requests[0].headers[PLUGIN_IDENTIFIER_HEADER] == IDENTIFIER
|
|
assert json.loads(requests[0].content)["data"]["credentials"] == {"api_key": "secret"}
|
|
|
|
|
|
def test_unbound_dispatch_keeps_legacy_headers_without_refetch(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(PluginService, "list_installations_from_ids", _unexpected_configuration_access)
|
|
headers = {"X-Plugin-ID": PLUGIN_ID}
|
|
|
|
prepare_dispatch_identity("plugin/tenant-a/dispatch/oauth/get_credentials", headers, None)
|
|
|
|
assert headers == {"X-Plugin-ID": PLUGIN_ID}
|
|
|
|
|
|
def test_dispatch_rejects_cross_plugin_identity() -> None:
|
|
with pytest.raises(ValueError, match="does not match"):
|
|
prepare_dispatch_identity(
|
|
"plugin/tenant-a/dispatch/tool/invoke", {"X-Plugin-ID": "langgenius/other"}, IDENTIFIER
|
|
)
|
|
|
|
|
|
def test_pinned_dispatch_requires_plugin_id() -> None:
|
|
with pytest.raises(ValueError, match="X-Plugin-ID is required"):
|
|
prepare_dispatch_identity("plugin/tenant-a/dispatch/tool/invoke", {}, IDENTIFIER)
|
|
|
|
|
|
def test_explicit_empty_identifier_cannot_fall_back_to_legacy_dispatch() -> None:
|
|
with pytest.raises(ValueError, match="must not be empty"):
|
|
BasePluginClient(plugin_unique_identifier="")
|