Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge

This commit is contained in:
Stephen Zhou
2026-10-15 20:02:49 +08:00
7 changed files with 769 additions and 492 deletions
@@ -1,4 +1,5 @@
import datetime
from dataclasses import dataclass
from inspect import unwrap
from types import SimpleNamespace
from unittest.mock import MagicMock, create_autospec, patch
@@ -42,7 +43,7 @@ from controllers.console.datasets.error import (
IndexingEstimateError,
)
from machinery.context import RequestContext
from models.knowledge_fs import KnowledgeFSUpgradeJobStatus, KnowledgeFSUpgradeStage
from models.knowledge_fs import KnowledgeFSUpgradeJob, KnowledgeFSUpgradeJobStatus, KnowledgeFSUpgradeStage
from services.data_source.entities.notion_import import NotionPageType
from services.dataset_knowledge_fs_upgrade_service import (
KnowledgeFSUpgradeConflictError,
@@ -344,32 +345,41 @@ def test_new_source_estimate_authorizes_before_execution(dataset_id, scene, loca
estimates.estimate_new_sources.assert_not_called()
def _upgrade_job(dataset_id: str = str(DATASET_ID), **overrides):
values = {
"id": "upgrade-job-1",
"old_dataset_id": dataset_id,
"new_control_space_id": None,
"status": KnowledgeFSUpgradeJobStatus.QUEUED,
"stage": KnowledgeFSUpgradeStage.VALIDATING,
"snapshot_at": datetime.datetime(2026, 8, 17, tzinfo=datetime.UTC),
"total_documents": 1,
"completed_documents": 0,
"total_sources": 0,
"completed_sources": 0,
"last_error_code": None,
"last_error_message": None,
"completed_at": None,
}
values.update(overrides)
return SimpleNamespace(**values)
def _upgrade_job(job_id: str = "upgrade-job-1") -> KnowledgeFSUpgradeJob:
job = KnowledgeFSUpgradeJob(
tenant_id="tenant-1",
old_dataset_id=str(DATASET_ID),
requested_by_account_id="account-1",
owner_account_id="account-1",
idempotency_key="upgrade-request-1",
snapshot_at=datetime.datetime(2026, 8, 17, tzinfo=datetime.UTC),
config_snapshot={},
permission_snapshot={},
app_binding_snapshot=[],
tag_ids_snapshot=[],
status=KnowledgeFSUpgradeJobStatus.QUEUED,
stage=KnowledgeFSUpgradeStage.VALIDATING,
total_documents=1,
)
job.id = job_id
return job
@dataclass(frozen=True)
class _UpgradeKnowledgeServices:
upgrades: KnowledgeUpgradeApplicationService
@dataclass(frozen=True)
class _UpgradeApplicationServices:
knowledge: _UpgradeKnowledgeServices
@pytest.fixture
def upgrades(monkeypatch):
service = create_autospec(KnowledgeUpgradeApplicationService, instance=True, spec_set=True)
monkeypatch.setattr(
controller, "application_services", lambda: SimpleNamespace(knowledge=SimpleNamespace(upgrades=service))
)
services = _UpgradeApplicationServices(knowledge=_UpgradeKnowledgeServices(upgrades=service))
monkeypatch.setattr(controller, "application_services", lambda: services)
return service
@@ -384,7 +394,7 @@ def test_upgrade_writes_require_dataset_edit_permission() -> None:
def test_upgrade_jobs_serializes_recoverable_jobs(app, upgrades):
upgrades.list_recoverable_jobs.return_value = [_upgrade_job(id="job-1")]
upgrades.list_recoverable_jobs.return_value = [_upgrade_job(job_id="job-1")]
with app.test_request_context("/datasets/knowledge-fs-upgrade-jobs"):
response = unwrap(DatasetKnowledgeFSUpgradeJobsApi.get)(DatasetKnowledgeFSUpgradeJobsApi(), CONTEXT)
upgrades.list_recoverable_jobs.assert_called_once_with(CONTEXT)
@@ -453,7 +463,7 @@ def test_upgrade_job_status_maps_missing_job_to_not_found(app, upgrades):
def test_upgrade_retry_returns_queued(app, upgrades):
upgrades.retry.return_value = _upgrade_job(id="job-1")
upgrades.retry.return_value = _upgrade_job(job_id="job-1")
with app.test_request_context(f"/datasets/{DATASET_ID}/knowledge-fs-upgrades/job-1"):
response, status = unwrap(DatasetKnowledgeFSUpgradeJobApi.post)(
DatasetKnowledgeFSUpgradeJobApi(), CONTEXT, DATASET_ID, "job-1"
@@ -36,7 +36,10 @@ from controllers.inner_api.plugin.plugin import (
PluginModelCatalogApi,
PluginUploadFileRequestApi,
)
from core.datasource.entities.datasource_entities import DatasourceProviderType
from core.plugin.entities.request import RequestInvokeDatasource, RequestInvokeTextEmbedding, RequestListModels
from core.workflow.file_reference import build_file_reference
from graphon.model_runtime.entities.model_entities import ModelType
from models import Account, Tenant
from tests.unit_tests.config_override import apply_config_overrides
from tests.unit_tests.model_factories import make_tenant
@@ -52,6 +55,10 @@ def _user() -> Account:
return user
def _text_embedding_payload() -> RequestInvokeTextEmbedding:
return RequestInvokeTextEmbedding(provider="openai", model="text-embedding-3-small", texts=["hello"])
def _extract_raw_post(cls):
"""Extract the raw post() method from a plugin endpoint class.
@@ -121,9 +128,9 @@ class TestPluginInvokeTextEmbeddingApi:
result = raw_post(
api_instance,
user_model=MagicMock(id="user-id"),
tenant_model=MagicMock(id="tenant-id"),
payload=MagicMock(),
user_model=_user(),
tenant_model=_tenant(),
payload=_text_embedding_payload(),
)
assert result == {
@@ -141,9 +148,9 @@ class TestPluginInvokeTextEmbeddingApi:
result = raw_post(
api_instance,
user_model=MagicMock(id="user-id"),
tenant_model=MagicMock(id="tenant-id"),
payload=MagicMock(),
user_model=_user(),
tenant_model=_tenant(),
payload=_text_embedding_payload(),
)
assert result == {
@@ -184,9 +191,9 @@ class TestPluginModelCatalogApi:
@patch("controllers.inner_api.plugin.plugin.PluginModelBackwardsInvocation")
def test_post_lists_models_in_the_resolved_tenant_scope(self, mock_invocation, api_instance, app: Flask):
mock_invocation.list_models.return_value = {"items": [], "next_offset": None}
mock_tenant = MagicMock(id="tenant-id")
mock_user = MagicMock(id="user-id")
mock_payload = MagicMock()
mock_tenant = _tenant()
mock_user = _user()
mock_payload = RequestListModels(model_type=ModelType.LLM)
raw_post = _extract_raw_post(PluginModelCatalogApi)
result = raw_post(
@@ -218,11 +225,19 @@ class TestPluginInvokeDatasourceApi:
api_instance,
app: Flask,
):
mock_tenant = MagicMock(id="tenant-id")
mock_user = MagicMock(id="user-id")
mock_payload = MagicMock()
invocation = MagicMock()
stream = MagicMock()
mock_tenant = _tenant()
mock_user = _user()
mock_payload = RequestInvokeDatasource(
tenant_id="tenant-id",
user_id="user-id",
provider="langgenius/notion_datasource/notion_datasource",
datasource="notion_datasource",
datasource_type=DatasourceProviderType.ONLINE_DOCUMENT,
credential_id="credential-id",
operation="get_online_document_pages",
)
invocation = object()
stream = object()
mock_invocation.invoke.return_value = invocation
mock_invocation.convert_to_event_stream.return_value = stream
mock_length_prefixed_response.return_value = "response"
@@ -0,0 +1,103 @@
"""Typed recording fakes shared by the KnowledgeFS controller tests.
Controllers reach KnowledgeFS through a runtime object whose components expose
plain methods. These fakes replace those components without mock objects:
``Recorder`` stores every call in a list and returns or raises what the test
configured, and ``FakeComponent`` exposes only the operations a test declares,
so an unexpected delegation fails with ``AttributeError``.
"""
from __future__ import annotations
from collections.abc import Callable, Iterable, Iterator
from dataclasses import dataclass, field
@dataclass(frozen=True)
class RecordedCall:
args: tuple[object, ...] = ()
kwargs: dict[str, object] = field(default_factory=dict)
def invocation(*args: object, **kwargs: object) -> RecordedCall:
return RecordedCall(args=args, kwargs=kwargs)
class Recorder:
"""Callable test double that records calls and returns, raises or delegates.
``raises`` is raised on every call, ``results`` is consumed one value per
call (exceptions in it are raised), and ``effect`` computes the result from
the call arguments. Otherwise ``return_value`` is returned.
"""
def __init__(
self,
return_value: object = None,
*,
raises: BaseException | None = None,
results: Iterable[object] | None = None,
effect: Callable[..., object] | None = None,
) -> None:
self.return_value = return_value
self.raises = raises
self.effect = effect
self._results: Iterator[object] | None = iter(results) if results is not None else None
self.calls: list[RecordedCall] = []
def __call__(self, *args: object, **kwargs: object) -> object:
self.calls.append(RecordedCall(args=args, kwargs=kwargs))
if self.raises is not None:
raise self.raises
if self._results is not None:
result = next(self._results)
if isinstance(result, BaseException):
raise result
return result
if self.effect is not None:
return self.effect(*args, **kwargs)
return self.return_value
@property
def last_call(self) -> RecordedCall:
assert self.calls, "expected at least one call"
return self.calls[-1]
@property
def only_call(self) -> RecordedCall:
assert len(self.calls) == 1, f"expected exactly one call, got {len(self.calls)}"
return self.calls[0]
class FakeComponent:
"""Runtime component exposing exactly the named operations given to it."""
def __init__(self, **operations: Recorder) -> None:
self._operations = operations
def __getattr__(self, name: str) -> Recorder:
operations = self.__dict__.get("_operations", {})
if name not in operations:
raise AttributeError(name)
return operations[name]
@dataclass
class FakeRuntime:
"""The subset of ``KnowledgeFSRuntime`` that controllers resolve per request."""
facade: FakeComponent = field(default_factory=FakeComponent)
application: FakeComponent = field(default_factory=FakeComponent)
control_plane: FakeComponent = field(default_factory=FakeComponent)
app_bindings: FakeComponent = field(default_factory=FakeComponent)
broker: FakeComponent = field(default_factory=FakeComponent)
service_api_authorization: FakeComponent = field(default_factory=FakeComponent)
def raising(error: BaseException) -> Callable[..., object]:
"""Return a plain callable that raises ``error`` when invoked."""
def operation(*_args: object, **_kwargs: object) -> object:
raise error
return operation
@@ -3,7 +3,6 @@ from __future__ import annotations
import inspect
import uuid
from typing import Protocol, runtime_checkable
from unittest.mock import Mock
import pytest
from flask import Flask
@@ -15,6 +14,13 @@ from controllers.openapi import bp as openapi_bp
from controllers.openapi.auth.context import Context
from controllers.openapi.auth.subjects import AccountSubject
from libs.oauth_bearer import AuthContext, TokenType
from tests.unit_tests.controllers.knowledge_fs_controller_fakes import (
FakeComponent,
FakeRuntime,
Recorder,
invocation,
raising,
)
@runtime_checkable
@@ -68,14 +74,17 @@ def test_list_controller_adapts_public_pagination_to_the_product_operation(monke
from controllers.openapi.knowledge_fs import KnowledgeFsEntryListApi
from services.knowledge_fs.product_dto import KnowledgeFSListResponse
facade = Mock()
facade.list_knowledge_fs.return_value = KnowledgeFSListResponse.model_validate(
{
"items": [],
"nextCursor": "opaque-next-page",
"path": "/knowledge",
"truncated": True,
}
facade = FakeComponent(
list_knowledge_fs=Recorder(
KnowledgeFSListResponse.model_validate(
{
"items": [],
"nextCursor": "opaque-next-page",
"path": "/knowledge",
"truncated": True,
}
)
)
)
monkeypatch.setattr(module, "_knowledge_fs_facade", lambda: facade)
account_id = uuid.uuid4()
@@ -103,7 +112,7 @@ def test_list_controller_adapts_public_pagination_to_the_product_operation(monke
expires_at=None,
)
),
session=Mock(spec=Session),
session=Session(),
view_args={},
),
)
@@ -118,8 +127,7 @@ def test_list_controller_adapts_public_pagination_to_the_product_operation(monke
"preview": None,
"truncated": True,
}
facade.list_knowledge_fs.assert_called_once()
call = facade.list_knowledge_fs.call_args.kwargs
call = facade.list_knowledge_fs.only_call.kwargs
assert call["tenant_id"] == "workspace-1"
assert call["account_id"] == str(account_id)
assert call["control_space_id"] == "space-1"
@@ -215,7 +223,6 @@ def test_each_custom_interface_delegates_with_an_internal_product_query(
from controllers.openapi import knowledge_fs as module
from services.knowledge_fs import product_dto
facade = Mock()
product_responses = {
"tree_knowledge_fs": product_dto.KnowledgeFSTreeResponse.model_validate(
{
@@ -247,8 +254,8 @@ def test_each_custom_interface_delegates_with_an_internal_product_query(
}
),
}
delegate = Mock(return_value=product_responses[facade_method])
facade.configure_mock(**{facade_method: delegate})
delegate = Recorder(product_responses[facade_method])
facade = FakeComponent(**{facade_method: delegate})
monkeypatch.setattr(module, "_knowledge_fs_facade", lambda: facade)
account_id = uuid.uuid4()
api_class, handler = {
@@ -291,15 +298,14 @@ def test_each_custom_interface_delegates_with_an_internal_product_query(
expires_at=None,
)
),
session=Mock(spec=Session),
session=Session(),
view_args={},
),
**({"body": public_model} if method_name == "post" else {"query": public_model}),
)
assert isinstance(result, BaseModel)
delegate.assert_called_once()
call = delegate.call_args.kwargs
call = delegate.only_call.kwargs
assert call["tenant_id"] == "workspace-1"
assert call["account_id"] == str(account_id)
assert call["control_space_id"] == "space-1"
@@ -322,13 +328,13 @@ def test_knowledge_fs_facade_uses_the_configured_runtime(monkeypatch: pytest.Mon
from controllers.openapi import knowledge_fs as module
session_maker = object()
facade = object()
get_runtime = Mock(return_value=Mock(facade=facade))
facade = FakeComponent()
get_runtime = Recorder(FakeRuntime(facade=facade))
monkeypatch.setattr(module.session_factory, "get_session_maker", lambda: session_maker)
monkeypatch.setattr(module, "get_knowledge_fs_runtime", get_runtime)
assert module._knowledge_fs_facade() is facade
get_runtime.assert_called_once_with(session_maker)
assert get_runtime.calls == [invocation(session_maker)]
def test_knowledge_fs_error_adapter_uses_stable_domain_errors() -> None:
@@ -365,10 +371,10 @@ def test_knowledge_fs_error_adapter_uses_stable_domain_errors() -> None:
)
for domain_error, openapi_error in mappings:
fail = module._knowledge_fs_errors(Mock(side_effect=domain_error))
guarded = module._knowledge_fs_errors(raising(domain_error))
with pytest.raises(openapi_error):
fail()
guarded()
@pytest.mark.parametrize("operation", ["ls", "tree", "grep", "find", "cat", "stat", "diff"])
@@ -4,12 +4,12 @@ import ast
import inspect
from collections.abc import Generator
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import UTC, datetime
from io import BytesIO
from pathlib import Path
from types import FunctionType, SimpleNamespace
from types import FunctionType
from typing import Never
from unittest.mock import MagicMock
import pytest
from flask import Flask
@@ -27,6 +27,10 @@ from controllers.console.knowledge_fs.error import (
from controllers.console.wraps import RBACPermission
from controllers.service_api import service_api_ns
from controllers.service_api.knowledge_fs import resources as service_resources
from models import Account
from models.enums import ApiTokenType
from models.model import ApiToken
from services.knowledge_fs.capability_broker import KnowledgeFSIssuedProductCapability
from services.knowledge_fs.control_space_commands import KnowledgeFSControlSpaceIntentConflictError
from services.knowledge_fs.download_service import KnowledgeFSDownloadUnavailableError
from services.knowledge_fs.object_storage import KnowledgeFSObjectMetadata
@@ -55,10 +59,64 @@ from services.knowledge_fs.service_api_authorization import (
KnowledgeFSServiceApiScopeError,
)
from tests.unit_tests.config_override import apply_config_overrides
from tests.unit_tests.controllers.knowledge_fs_controller_fakes import FakeComponent, FakeRuntime, Recorder, invocation
_API_ROOT = Path(__file__).resolve().parents[3]
@dataclass(frozen=True)
class _FacadeRuntime[T]:
facade: T
@dataclass(frozen=True)
class _BrokerRuntime[T]:
broker: T
def _account(account_id: str = "account-1") -> Account:
account = Account(name="Test User", email="user@example.com")
account.id = account_id
return account
def _dataset_api_token(tenant_id: str | None = "tenant-1") -> ApiToken:
api_token = ApiToken()
api_token.id = "token-1"
api_token.tenant_id = tenant_id
api_token.type = ApiTokenType.DATASET
api_token.token = "dataset-test-key"
return api_token
def _issued_capability(
*, token: str, operation_id: str, knowledge_space_id: str = "space-1"
) -> KnowledgeFSIssuedProductCapability:
return KnowledgeFSIssuedProductCapability(
token=token,
expires_at=datetime(2030, 1, 1, tzinfo=UTC),
operation_id=operation_id,
knowledge_space_id=knowledge_space_id,
knowledge_space_revision=1,
trace_id="trace-1",
)
def _service_profile(control_space_id: str) -> KnowledgeFSServiceApiProfile:
return KnowledgeFSServiceApiProfile(
tenant_id="tenant-1",
control_space_id=control_space_id,
api_token_id="token-1",
principal_id="token-1",
knowledge_space_id="space-1",
knowledge_space_revision=1,
membership_epoch=0,
space_acl_epoch=0,
external_access_epoch=0,
content_policy_revision=0,
)
def _rbac_wrapper(view: FunctionType) -> FunctionType:
current = inspect.unwrap(
view,
@@ -194,12 +252,19 @@ def test_console_and_service_api_routes_are_registered() -> None:
def test_initial_website_preview_job_routes_delegate_to_namespace_capabilities(
monkeypatch: pytest.MonkeyPatch,
) -> None:
account = SimpleNamespace(id="account-1")
preview_service = SimpleNamespace(require_visible_credential=MagicMock())
facade = SimpleNamespace(
create_namespace_source_preview=MagicMock(return_value=SimpleNamespace(job_id="job-1")),
get_namespace_source_preview=MagicMock(
return_value=KnowledgeFSNamespacePreviewJobResponse(
account = _account()
preview_service = FakeComponent(require_visible_credential=Recorder())
facade = FakeComponent(
create_namespace_source_preview=Recorder(
KnowledgeFSNamespacePreviewJobResponse(
job_id="job-1",
status="queued",
configuration_fingerprint="f" * 64,
expires_at="2030-01-01T00:00:00Z",
)
),
get_namespace_source_preview=Recorder(
KnowledgeFSNamespacePreviewJobResponse(
job_id="job-1",
status="completed",
configuration_fingerprint="f" * 64,
@@ -213,8 +278,8 @@ def test_initial_website_preview_job_routes_delegate_to_namespace_capabilities(
],
)
),
cancel_namespace_source_preview=MagicMock(
return_value=KnowledgeFSNamespacePreviewJobResponse(
cancel_namespace_source_preview=Recorder(
KnowledgeFSNamespacePreviewJobResponse(
job_id="job-1",
status="consumed",
configuration_fingerprint="f" * 64,
@@ -224,7 +289,7 @@ def test_initial_website_preview_job_routes_delegate_to_namespace_capabilities(
)
monkeypatch.setattr(console_resources, "current_account_with_tenant", lambda: (account, "tenant-1"))
monkeypatch.setattr(console_resources, "KnowledgeFSInitialSourcePreviewService", lambda _: preview_service)
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
app = Flask(__name__)
with app.test_request_context(
@@ -243,8 +308,8 @@ def test_initial_website_preview_job_routes_delegate_to_namespace_capabilities(
)
assert status == 202
assert response == {"job_id": "job-1", "status": "pending"}
preview_service.require_visible_credential.assert_called_once()
facade.create_namespace_source_preview.assert_called_once()
assert len(preview_service.require_visible_credential.calls) == 1
assert len(facade.create_namespace_source_preview.calls) == 1
with app.test_request_context():
response = inspect.unwrap(console_resources.KnowledgeFSInitialSourcePreviewJobApi.get)(
@@ -301,12 +366,12 @@ def test_document_download_routes_deny_callers_without_dataset_download_permissi
method_name: FunctionType,
path_args: dict[str, str],
) -> None:
permission_gate = MagicMock(side_effect=Forbidden())
permission_gate = Recorder(raises=Forbidden())
apply_config_overrides(monkeypatch, RBAC_ENABLED=True)
monkeypatch.setattr(
common_wraps,
"current_account_with_tenant",
lambda: (SimpleNamespace(id="account-1"), "tenant-1"),
lambda: (_account(), "tenant-1"),
)
monkeypatch.setattr("controllers.common.rbac.checks.RBACService.CheckAccess.check", permission_gate)
# The download routes carry the knowledge rate limit too; it needs a logged-in account.
@@ -321,13 +386,15 @@ def test_document_download_routes_deny_callers_without_dataset_download_permissi
with pytest.raises(Forbidden):
permission_wrapper(api_class(), **path_args)
permission_gate.assert_called_once_with(
"tenant-1",
"account-1",
scene=RBACPermission.DATASET_DOCUMENT_DOWNLOAD,
resource_type=None,
resource_id=None,
)
assert permission_gate.calls == [
invocation(
"tenant-1",
"account-1",
scene=RBACPermission.DATASET_DOCUMENT_DOWNLOAD,
resource_type=None,
resource_id=None,
)
]
def test_single_document_download_allows_dataset_download_permission_and_keeps_tenant_scope(
@@ -341,20 +408,20 @@ def test_single_document_download_allows_dataset_download_permission_and_keeps_t
sha256="a" * 64,
size_bytes=4,
)
facade = SimpleNamespace(prepare_logical_document_download=MagicMock(return_value=descriptor))
download_service = SimpleNamespace(load_stream=MagicMock(return_value=iter((b"body",))))
permission_gate = MagicMock()
facade = FakeComponent(prepare_logical_document_download=Recorder(descriptor))
download_service = FakeComponent(load_stream=Recorder(iter((b"body",))))
permission_gate = Recorder(True)
apply_config_overrides(monkeypatch, RBAC_ENABLED=True)
monkeypatch.setattr(
common_wraps,
"current_account_with_tenant",
lambda: (SimpleNamespace(id="account-1"), "tenant-1"),
lambda: (_account(), "tenant-1"),
)
monkeypatch.setattr("controllers.common.rbac.checks.RBACService.CheckAccess.check", permission_gate)
# The download routes carry the knowledge rate limit too; it needs a logged-in account.
monkeypatch.setattr(console_wraps, "check_knowledge_rate_limit", lambda: None)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
monkeypatch.setattr(console_resources, "KnowledgeFSDownloadService", lambda: download_service)
permission_wrapper = _rbac_wrapper(console_resources.KnowledgeFSSpaceLogicalDocumentDownloadApi.get)
app = Flask(__name__)
@@ -368,42 +435,44 @@ def test_single_document_download_allows_dataset_download_permission_and_keeps_t
assert response.get_data() == b"body"
assert response.headers["Content-Disposition"] == "attachment; filename*=UTF-8''guide.md"
permission_gate.assert_called_once_with(
"tenant-1",
"account-1",
scene=RBACPermission.DATASET_DOCUMENT_DOWNLOAD,
resource_type=None,
resource_id=None,
)
facade.prepare_logical_document_download.assert_called_once_with(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-1",
document_id="document-1",
)
assert permission_gate.calls == [
invocation(
"tenant-1",
"account-1",
scene=RBACPermission.DATASET_DOCUMENT_DOWNLOAD,
resource_type=None,
resource_id=None,
)
]
assert facade.prepare_logical_document_download.calls == [
invocation(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-1",
document_id="document-1",
)
]
def test_single_document_download_preserves_resource_not_found_after_permission_allows(
monkeypatch: pytest.MonkeyPatch,
) -> None:
facade = SimpleNamespace(
prepare_logical_document_download=MagicMock(
side_effect=KnowledgeFSProductResourceNotFoundError("document not found")
)
facade = FakeComponent(
prepare_logical_document_download=Recorder(raises=KnowledgeFSProductResourceNotFoundError("document not found"))
)
permission_gate = MagicMock()
download_service_factory = MagicMock()
permission_gate = Recorder(True)
download_service_factory = Recorder()
apply_config_overrides(monkeypatch, RBAC_ENABLED=True)
monkeypatch.setattr(
common_wraps,
"current_account_with_tenant",
lambda: (SimpleNamespace(id="account-1"), "tenant-1"),
lambda: (_account(), "tenant-1"),
)
monkeypatch.setattr("controllers.common.rbac.checks.RBACService.CheckAccess.check", permission_gate)
# The download routes carry the knowledge rate limit too; it needs a logged-in account.
monkeypatch.setattr(console_wraps, "check_knowledge_rate_limit", lambda: None)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
monkeypatch.setattr(console_resources, "KnowledgeFSDownloadService", download_service_factory)
permission_wrapper = _rbac_wrapper(console_resources.KnowledgeFSSpaceLogicalDocumentDownloadApi.get)
app = Flask(__name__)
@@ -415,14 +484,16 @@ def test_single_document_download_preserves_resource_not_found_after_permission_
document_id="document-1",
)
permission_gate.assert_called_once()
facade.prepare_logical_document_download.assert_called_once_with(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="foreign-control",
document_id="document-1",
)
download_service_factory.assert_not_called()
assert len(permission_gate.calls) == 1
assert facade.prepare_logical_document_download.calls == [
invocation(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="foreign-control",
document_id="document-1",
)
]
assert download_service_factory.calls == []
def test_single_document_download_maps_storage_unavailable_after_permission_allows(
@@ -436,22 +507,22 @@ def test_single_document_download_maps_storage_unavailable_after_permission_allo
sha256="a" * 64,
size_bytes=4,
)
facade = SimpleNamespace(prepare_logical_document_download=MagicMock(return_value=descriptor))
download_service = SimpleNamespace(
load_stream=MagicMock(side_effect=KnowledgeFSDownloadUnavailableError("storage unavailable"))
facade = FakeComponent(prepare_logical_document_download=Recorder(descriptor))
download_service = FakeComponent(
load_stream=Recorder(raises=KnowledgeFSDownloadUnavailableError("storage unavailable"))
)
permission_gate = MagicMock()
permission_gate = Recorder(True)
apply_config_overrides(monkeypatch, RBAC_ENABLED=True)
monkeypatch.setattr(
common_wraps,
"current_account_with_tenant",
lambda: (SimpleNamespace(id="account-1"), "tenant-1"),
lambda: (_account(), "tenant-1"),
)
monkeypatch.setattr("controllers.common.rbac.checks.RBACService.CheckAccess.check", permission_gate)
# The download routes carry the knowledge rate limit too; it needs a logged-in account.
monkeypatch.setattr(console_wraps, "check_knowledge_rate_limit", lambda: None)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
monkeypatch.setattr(console_resources, "KnowledgeFSDownloadService", lambda: download_service)
permission_wrapper = _rbac_wrapper(console_resources.KnowledgeFSSpaceLogicalDocumentDownloadApi.get)
app = Flask(__name__)
@@ -463,8 +534,8 @@ def test_single_document_download_maps_storage_unavailable_after_permission_allo
document_id="document-1",
)
permission_gate.assert_called_once()
download_service.load_stream.assert_called_once_with(descriptor)
assert len(permission_gate.calls) == 1
assert download_service.load_stream.calls == [invocation(descriptor)]
def test_batch_document_download_maps_storage_unavailable_after_permission_allows(
@@ -478,22 +549,26 @@ def test_batch_document_download_maps_storage_unavailable_after_permission_allow
sha256="a" * 64,
size_bytes=4,
)
facade = SimpleNamespace(prepare_logical_document_download=MagicMock(return_value=descriptor))
zip_context = MagicMock()
zip_context.__enter__.side_effect = KnowledgeFSDownloadUnavailableError("storage unavailable")
download_service = SimpleNamespace(build_zip_tempfile=MagicMock(return_value=zip_context))
permission_gate = MagicMock()
facade = FakeComponent(prepare_logical_document_download=Recorder(descriptor))
@contextmanager
def unavailable_zip() -> Generator[Never]:
raise KnowledgeFSDownloadUnavailableError("storage unavailable")
yield
download_service = FakeComponent(build_zip_tempfile=Recorder(unavailable_zip()))
permission_gate = Recorder(True)
apply_config_overrides(monkeypatch, RBAC_ENABLED=True)
monkeypatch.setattr(
common_wraps,
"current_account_with_tenant",
lambda: (SimpleNamespace(id="account-1"), "tenant-1"),
lambda: (_account(), "tenant-1"),
)
monkeypatch.setattr("controllers.common.rbac.checks.RBACService.CheckAccess.check", permission_gate)
# The download routes carry the knowledge rate limit too; it needs a logged-in account.
monkeypatch.setattr(console_wraps, "check_knowledge_rate_limit", lambda: None)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
monkeypatch.setattr(console_resources, "KnowledgeFSDownloadService", lambda: download_service)
permission_wrapper = _rbac_wrapper(console_resources.KnowledgeFSSpaceLogicalDocumentsDownloadApi.post)
app = Flask(__name__)
@@ -507,8 +582,8 @@ def test_batch_document_download_maps_storage_unavailable_after_permission_allow
control_space_id="control-1",
)
permission_gate.assert_called_once()
download_service.build_zip_tempfile.assert_called_once_with([descriptor])
assert len(permission_gate.calls) == 1
assert download_service.build_zip_tempfile.calls == [invocation([descriptor])]
def test_knowledge_fs_request_and_response_schemas_are_registered() -> None:
@@ -631,9 +706,9 @@ def test_document_multimodal_manifest_console_bff_exposes_only_authorized_asset_
f"tenant-1/spaces/018f0d60-7a49-7cc2-9c1b-5b36f18f2c43/documents/{document_id}/assets/figure-1-full.png"
)
)
facade = SimpleNamespace(get_document_multimodal_manifest=MagicMock(return_value=manifest))
facade = FakeComponent(get_document_multimodal_manifest=Recorder(manifest))
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
apply_config_overrides(monkeypatch, CONSOLE_API_URL="")
app = Flask(__name__)
@@ -653,12 +728,14 @@ def test_document_multimodal_manifest_console_bff_exposes_only_authorized_asset_
assert item["parse_element_id"] == "parse-element-1"
assert "objectKey" not in str(response)
assert "sha256" not in str(response)
facade.get_document_multimodal_manifest.assert_called_once_with(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-1",
document_id=document_id,
)
assert facade.get_document_multimodal_manifest.calls == [
invocation(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-1",
document_id=document_id,
)
]
def test_document_multimodal_asset_console_bff_reauthorizes_and_streams_scoped_images(
@@ -669,10 +746,10 @@ def test_document_multimodal_asset_console_bff_reauthorizes_and_streams_scoped_i
f"tenant-1/spaces/018f0d60-7a49-7cc2-9c1b-5b36f18f2c43/documents/{document_id}/assets/figure-1-full.png"
)
manifest = _document_multimodal_manifest(object_key=object_key)
facade = SimpleNamespace(get_document_multimodal_manifest=MagicMock(return_value=manifest))
object_storage = SimpleNamespace(
head_object=MagicMock(
return_value=KnowledgeFSObjectMetadata(
facade = FakeComponent(get_document_multimodal_manifest=Recorder(manifest))
object_storage = FakeComponent(
head_object=Recorder(
KnowledgeFSObjectMetadata(
checksum_sha256_base64="checksum",
content_type="image/png",
key=object_key,
@@ -681,10 +758,10 @@ def test_document_multimodal_asset_console_bff_reauthorizes_and_streams_scoped_i
)
)
)
download_service = SimpleNamespace(load_stream=MagicMock(return_value=iter((b"im", b"age"))))
download_service_factory = MagicMock(return_value=download_service)
download_service = FakeComponent(load_stream=Recorder(iter((b"im", b"age"))))
download_service_factory = Recorder(download_service)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
monkeypatch.setattr(console_resources, "KnowledgeFSObjectStorageService", lambda: object_storage)
monkeypatch.setattr(console_resources, "KnowledgeFSDownloadService", download_service_factory)
app = Flask(__name__)
@@ -703,9 +780,9 @@ def test_document_multimodal_asset_console_bff_reauthorizes_and_streams_scoped_i
assert response.headers["Content-Disposition"] == "inline"
assert response.headers["Content-Security-Policy"] == "default-src 'none'; sandbox"
assert response.headers["X-Content-Type-Options"] == "nosniff"
object_storage.head_object.assert_called_once_with(key=object_key)
download_service_factory.assert_called_once_with(object_storage=object_storage)
download_service.load_stream.assert_called_once()
assert object_storage.head_object.calls == [invocation(key=object_key)]
assert download_service_factory.calls == [invocation(object_storage=object_storage)]
assert len(download_service.load_stream.calls) == 1
def test_document_multimodal_asset_console_bff_rejects_cross_document_object_keys(
@@ -718,10 +795,10 @@ def test_document_multimodal_asset_console_bff_rejects_cross_document_object_key
"documents/018f0d60-7a49-7cc2-9c1b-5b36f18f2cff/assets/foreign.png"
)
)
facade = SimpleNamespace(get_document_multimodal_manifest=MagicMock(return_value=manifest))
object_storage_factory = MagicMock()
facade = FakeComponent(get_document_multimodal_manifest=Recorder(manifest))
object_storage_factory = Recorder()
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
monkeypatch.setattr(console_resources, "KnowledgeFSObjectStorageService", object_storage_factory)
app = Flask(__name__)
get = inspect.unwrap(console_resources.KnowledgeFSSpaceDocumentMultimodalAssetApi.get)
@@ -734,7 +811,7 @@ def test_document_multimodal_asset_console_bff_rejects_cross_document_object_key
item_id="figure:1",
)
object_storage_factory.assert_not_called()
assert object_storage_factory.calls == []
def test_source_workflow_import_contract_is_discriminated_idempotent_and_accepted() -> None:
@@ -785,7 +862,7 @@ def test_small_file_console_bff_reads_only_through_facade_and_returns_no_capabil
)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=Facade()))
monkeypatch.setattr(console_resources, "_console_services", lambda: _FacadeRuntime(facade=Facade()))
app = Flask(__name__)
with app.test_request_context(
@@ -851,7 +928,7 @@ def test_document_upload_console_bff_reads_only_through_facade_and_returns_accep
)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=Facade()))
monkeypatch.setattr(console_resources, "_console_services", lambda: _FacadeRuntime(facade=Facade()))
app = Flask(__name__)
with app.test_request_context(
@@ -876,11 +953,14 @@ def test_document_upload_console_bff_reads_only_through_facade_and_returns_accep
def test_staged_document_claim_passes_only_upload_id_to_the_admission_service(
monkeypatch: pytest.MonkeyPatch,
) -> None:
staged_uploads = MagicMock()
staged_uploads.claim.return_value = KnowledgeFSDocumentStagedUploadAcceptedResponse(
upload_id="staged-1",
document_asset_id="asset-1",
compilation_job_id="job-1",
staged_uploads = FakeComponent(
claim=Recorder(
KnowledgeFSDocumentStagedUploadAcceptedResponse(
upload_id="staged-1",
document_asset_id="asset-1",
compilation_job_id="job-1",
)
)
)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_staged_uploads", lambda: staged_uploads)
@@ -897,8 +977,8 @@ def test_staged_document_claim_passes_only_upload_id_to_the_admission_service(
"document_asset_id": "asset-1",
"compilation_job_id": "job-1",
}
staged_uploads.claim.assert_called_once()
assert staged_uploads.claim.call_args.kwargs["payload"].upload_id == "staged-1"
assert len(staged_uploads.claim.calls) == 1
assert staged_uploads.claim.last_call.kwargs["payload"].upload_id == "staged-1"
@pytest.mark.parametrize("file_size_limit_mb", [15, 50])
@@ -922,8 +1002,7 @@ def test_workspace_staging_upload_persists_before_space_creation(
admission_events.append("release")
admission = RecordingAdmission()
staged_uploads = MagicMock()
staged_uploads.stage.return_value = KnowledgeFSStagedUploadResponse(
staged = KnowledgeFSStagedUploadResponse(
id="staged-1",
file_name="notes.md",
content_type="text/markdown",
@@ -935,10 +1014,10 @@ def test_workspace_staging_upload_persists_before_space_creation(
def stage(**_: object) -> KnowledgeFSStagedUploadResponse:
assert admission.active
admission_events.append("stage")
return staged_uploads.stage.return_value
return staged
staged_uploads.stage.side_effect = stage
account = SimpleNamespace(id="account-1")
staged_uploads = FakeComponent(stage=Recorder(effect=stage))
account = _account()
monkeypatch.setattr(console_resources, "current_account_with_tenant", lambda: (account, "tenant-1"))
monkeypatch.setattr(console_resources, "_staged_uploads", lambda: staged_uploads)
monkeypatch.setattr(console_resources, "DEFAULT_KNOWLEDGE_FS_BUFFERED_UPLOAD_ADMISSION", admission)
@@ -960,14 +1039,16 @@ def test_workspace_staging_upload_persists_before_space_creation(
assert status == 201
assert response["id"] == "staged-1"
assert admission_events == [f"admit:{file_size_limit_mb * 1024 * 1024}", "stage", "release"]
staged_uploads.stage.assert_called_once_with(
tenant_id="tenant-1",
account=account,
file_name="notes.md",
content_type="text/markdown",
body=b"# Notes",
file_size_limit_mb=file_size_limit_mb,
)
assert staged_uploads.stage.calls == [
invocation(
tenant_id="tenant-1",
account=account,
file_name="notes.md",
content_type="text/markdown",
body=b"# Notes",
file_size_limit_mb=file_size_limit_mb,
)
]
def test_workspace_staging_zero_byte_plan_limit_returns_request_too_large_before_admission(
@@ -978,8 +1059,8 @@ def test_workspace_staging_zero_byte_plan_limit_returns_request_too_large_before
_ = reserved_bytes
raise AssertionError("a disabled upload limit must fail before admission")
staged_uploads = MagicMock()
account = SimpleNamespace(id="account-1")
staged_uploads = FakeComponent(stage=Recorder())
account = _account()
monkeypatch.setattr(console_resources, "current_account_with_tenant", lambda: (account, "tenant-1"))
monkeypatch.setattr(console_resources, "_staged_uploads", lambda: staged_uploads)
monkeypatch.setattr(console_resources, "DEFAULT_KNOWLEDGE_FS_BUFFERED_UPLOAD_ADMISSION", FailingAdmission())
@@ -1000,7 +1081,7 @@ def test_workspace_staging_zero_byte_plan_limit_returns_request_too_large_before
post(console_resources.KnowledgeFSStagedUploadsApi())
assert rejected.value.status_code == 413
staged_uploads.stage.assert_not_called()
assert staged_uploads.stage.calls == []
def test_logical_document_delete_accepts_initial_row_version(
@@ -1031,7 +1112,7 @@ def test_logical_document_delete_accepts_initial_row_version(
)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=Facade()))
monkeypatch.setattr(console_resources, "_console_services", lambda: _FacadeRuntime(facade=Facade()))
app = Flask(__name__)
with app.test_request_context(
@@ -1261,7 +1342,7 @@ def test_jwks_http_resource_returns_only_public_keys(monkeypatch: pytest.MonkeyP
{"alg": "RS256", "e": "AQAB", "kid": "previous", "kty": "RSA", "n": "modulus-2", "use": "sig"},
]
}
issuer = SimpleNamespace(public_jwks=lambda: public_jwks)
issuer = FakeComponent(public_jwks=Recorder(public_jwks))
apply_config_overrides(
monkeypatch,
KNOWLEDGE_FS_CAPABILITY_V2_ENABLED=True,
@@ -1293,14 +1374,9 @@ def test_service_profile_authorizes_the_dataset_key_for_the_route_space(monkeypa
external_access_epoch=0,
content_policy_revision=0,
)
authorization = MagicMock()
authorization.authorize.return_value = profile
runtime = SimpleNamespace(service_api_authorization=authorization)
monkeypatch.setattr(
service_resources,
"validate_and_get_api_token",
MagicMock(return_value=SimpleNamespace(id="token-1", tenant_id="tenant-1")),
)
authorization = FakeComponent(authorize=Recorder(profile))
runtime = FakeRuntime(service_api_authorization=authorization)
monkeypatch.setattr(service_resources, "validate_and_get_api_token", Recorder(_dataset_api_token()))
app = Flask(__name__)
with app.test_request_context(headers={"Authorization": "Bearer dataset-test-key"}):
result = service_resources._profile(
@@ -1310,22 +1386,24 @@ def test_service_profile_authorizes_the_dataset_key_for_the_route_space(monkeypa
)
assert result is profile
authorization.authorize.assert_called_once_with(
api_token_id="token-1",
tenant_id="tenant-1",
control_space_id="control-2",
)
assert authorization.authorize.calls == [
invocation(
api_token_id="token-1",
tenant_id="tenant-1",
control_space_id="control-2",
)
]
def test_service_profile_applies_the_knowledge_rate_limit_before_authorizing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Every KnowledgeFS service route shares the legacy dataset service API rate limit."""
authorization = MagicMock()
runtime = SimpleNamespace(service_api_authorization=authorization)
api_token = SimpleNamespace(id="token-1", tenant_id="tenant-1")
monkeypatch.setattr(service_resources, "validate_and_get_api_token", MagicMock(return_value=api_token))
rate_limit = MagicMock(side_effect=Forbidden("rate limited"))
authorization = FakeComponent(authorize=Recorder())
runtime = FakeRuntime(service_api_authorization=authorization)
api_token = _dataset_api_token()
monkeypatch.setattr(service_resources, "validate_and_get_api_token", Recorder(api_token))
rate_limit = Recorder(raises=Forbidden("rate limited"))
monkeypatch.setattr(service_resources, "check_knowledge_rate_limit", rate_limit)
with pytest.raises(Forbidden, match="rate limited"):
@@ -1335,18 +1413,14 @@ def test_service_profile_applies_the_knowledge_rate_limit_before_authorizing(
control_space_id="control-2",
)
rate_limit.assert_called_once_with(api_token)
authorization.authorize.assert_not_called()
assert rate_limit.calls == [invocation(api_token)]
assert authorization.authorize.calls == []
def test_service_profile_rejects_a_dataset_key_without_a_workspace(monkeypatch: pytest.MonkeyPatch) -> None:
authorization = MagicMock()
runtime = SimpleNamespace(service_api_authorization=authorization)
monkeypatch.setattr(
service_resources,
"validate_and_get_api_token",
MagicMock(return_value=SimpleNamespace(id="token-1", tenant_id=None)),
)
authorization = FakeComponent(authorize=Recorder())
runtime = FakeRuntime(service_api_authorization=authorization)
monkeypatch.setattr(service_resources, "validate_and_get_api_token", Recorder(_dataset_api_token(tenant_id=None)))
with pytest.raises(KnowledgeFSServiceApiAuthorizationError, match="not workspace-scoped"):
service_resources._profile(
@@ -1355,7 +1429,7 @@ def test_service_profile_rejects_a_dataset_key_without_a_workspace(monkeypatch:
control_space_id="control-2",
)
authorization.authorize.assert_not_called()
assert authorization.authorize.calls == []
def test_product_modules_do_not_import_dify_dataset_or_document_services() -> None:
@@ -1415,20 +1489,16 @@ def test_query_stream_capability_issues_exact_space_grant_without_token_in_url(
calls: list[dict[str, object]] = []
class Broker:
def issue_interactive(self, **kwargs: object) -> SimpleNamespace:
def issue_interactive(self, **kwargs: object) -> KnowledgeFSIssuedProductCapability:
calls.append(kwargs)
return SimpleNamespace(
token="query-capability",
trace_id="trace-1",
expires_at=datetime(2030, 1, 1, tzinfo=UTC),
)
return _issued_capability(token="query-capability", operation_id="createQuery")
apply_config_overrides(monkeypatch, CONSOLE_API_URL="https://dify.test")
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(
console_resources,
"_console_services",
lambda: SimpleNamespace(broker=Broker()),
lambda: _BrokerRuntime(broker=Broker()),
)
app = Flask(__name__)
@@ -1458,21 +1528,16 @@ def test_query_admission_binds_validated_mode_to_resolved_kfs_space(monkeypatch:
calls: list[dict[str, object]] = []
class Broker:
def issue_interactive(self, **kwargs: object) -> SimpleNamespace:
def issue_interactive(self, **kwargs: object) -> KnowledgeFSIssuedProductCapability:
calls.append(kwargs)
return SimpleNamespace(
token="query-capability",
trace_id="trace-1",
expires_at=datetime(2030, 1, 1, tzinfo=UTC),
knowledge_space_id="space-1",
)
return _issued_capability(token="query-capability", operation_id="createQuery")
apply_config_overrides(monkeypatch, CONSOLE_API_URL="https://dify.test")
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(
console_resources,
"_console_services",
lambda: SimpleNamespace(broker=Broker()),
lambda: _BrokerRuntime(broker=Broker()),
)
app = Flask(__name__)
@@ -1503,10 +1568,14 @@ def test_query_admission_binds_validated_mode_to_resolved_kfs_space(monkeypatch:
def test_console_query_stream_proxy_forwards_only_the_admitted_capability(
monkeypatch: pytest.MonkeyPatch,
) -> None:
close = MagicMock()
facade = SimpleNamespace(
stream_query=MagicMock(
return_value=KnowledgeFSRemoteSSEResponse(
closed: list[str] = []
def close() -> None:
closed.append("closed")
facade = FakeComponent(
stream_query=Recorder(
KnowledgeFSRemoteSSEResponse(
status_code=200,
headers=(
("content-type", "text/event-stream"),
@@ -1517,7 +1586,7 @@ def test_console_query_stream_proxy_forwards_only_the_admitted_capability(
)
)
)
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(console_resources, "_console_services", lambda: FakeRuntime(facade=facade))
app = Flask(__name__)
with app.test_request_context(
@@ -1541,8 +1610,8 @@ def test_console_query_stream_proxy_forwards_only_the_admitted_capability(
assert response.headers["Content-Type"] == "text/event-stream"
assert response.headers["X-Accel-Buffering"] == "no"
assert b"".join(response.response) == b'event: answer\ndata: {"answer":"ok"}\n\n'
close.assert_called_once_with()
call = facade.stream_query.call_args.kwargs
assert closed == ["closed"]
call = facade.stream_query.last_call.kwargs
assert call["capability_token"] == "capability-token"
assert call["trace_id"] == "trace-1"
assert call["payload"].knowledge_space_id == "space-1"
@@ -1552,10 +1621,14 @@ def test_console_query_stream_proxy_forwards_only_the_admitted_capability(
def test_service_query_stream_proxy_forwards_only_the_admitted_capability(
monkeypatch: pytest.MonkeyPatch,
) -> None:
close = MagicMock()
facade = SimpleNamespace(
stream_query=MagicMock(
return_value=KnowledgeFSRemoteSSEResponse(
closed: list[str] = []
def close() -> None:
closed.append("closed")
facade = FakeComponent(
stream_query=Recorder(
KnowledgeFSRemoteSSEResponse(
status_code=200,
headers=(
("content-type", "text/event-stream"),
@@ -1566,7 +1639,7 @@ def test_service_query_stream_proxy_forwards_only_the_admitted_capability(
)
)
)
monkeypatch.setattr(service_resources, "_runtime", lambda: SimpleNamespace(facade=facade))
monkeypatch.setattr(service_resources, "_runtime", lambda: FakeRuntime(facade=facade))
app = Flask(__name__)
with app.test_request_context(
@@ -1590,8 +1663,8 @@ def test_service_query_stream_proxy_forwards_only_the_admitted_capability(
assert response.headers["Content-Type"] == "text/event-stream"
assert response.headers["X-Accel-Buffering"] == "no"
assert b"".join(response.response) == b'event: answer\ndata: {"answer":"ok"}\n\n'
close.assert_called_once_with()
call = facade.stream_query.call_args.kwargs
assert closed == ["closed"]
call = facade.stream_query.last_call.kwargs
assert call["capability_token"] == "capability-token"
assert call["trace_id"] == "trace-1"
assert call["payload"].knowledge_space_id == "space-1"
@@ -1624,15 +1697,11 @@ def test_task_stream_capability_uses_broker(
calls: list[dict[str, object]] = []
class Broker:
def issue_interactive(self, **kwargs: object) -> SimpleNamespace:
def issue_interactive(self, **kwargs: object) -> KnowledgeFSIssuedProductCapability:
calls.append(kwargs)
return SimpleNamespace(
token="direct-capability",
expires_at=datetime(2030, 1, 1, tzinfo=UTC),
knowledge_space_id="space-1",
)
return _issued_capability(token="direct-capability", operation_id="streamResearchTask")
runtime = SimpleNamespace(broker=Broker())
runtime = _BrokerRuntime(broker=Broker())
apply_config_overrides(monkeypatch, CONSOLE_API_URL="https://dify.test")
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
@@ -1656,29 +1725,21 @@ def test_task_stream_capability_uses_broker(
def test_service_query_admission_uses_broker(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[dict[str, object]] = []
profile = SimpleNamespace(tenant_id="tenant-1", control_space_id="control-1")
authorization = MagicMock()
authorization.authorize.return_value = profile
profile = _service_profile("control-1")
authorization = FakeComponent(authorize=Recorder(profile))
class Broker:
def issue_service(self, **kwargs: object) -> SimpleNamespace:
def issue_service(self, **kwargs: object) -> KnowledgeFSIssuedProductCapability:
calls.append(kwargs)
return SimpleNamespace(
token="service-capability",
trace_id="trace-1",
expires_at=datetime(2030, 1, 1, tzinfo=UTC),
knowledge_space_id="space-1",
)
return _issued_capability(token="service-capability", operation_id="createQuery")
runtime = SimpleNamespace(
service_api_authorization=authorization,
broker=Broker(),
)
monkeypatch.setattr(
service_resources,
"validate_and_get_api_token",
MagicMock(return_value=SimpleNamespace(id="token-1", tenant_id="tenant-1")),
)
@dataclass(frozen=True)
class ServiceRuntime:
service_api_authorization: FakeComponent
broker: Broker
runtime = ServiceRuntime(service_api_authorization=authorization, broker=Broker())
monkeypatch.setattr(service_resources, "validate_and_get_api_token", Recorder(_dataset_api_token()))
apply_config_overrides(monkeypatch, SERVICE_API_URL="https://api.dify.test")
monkeypatch.setattr(service_resources, "_runtime", lambda: runtime)
app = Flask(__name__)
@@ -1694,11 +1755,13 @@ def test_service_query_admission_uses_broker(monkeypatch: pytest.MonkeyPatch) ->
assert response["trace_id"] == "trace-1"
assert response["request"]["knowledgeSpaceId"] == "space-1"
assert response["url"] == "https://api.dify.test/v1/knowledge-fs/query-stream"
authorization.authorize.assert_called_once_with(
api_token_id="token-1",
tenant_id="tenant-1",
control_space_id="control-1",
)
assert authorization.authorize.calls == [
invocation(
api_token_id="token-1",
tenant_id="tenant-1",
control_space_id="control-1",
)
]
assert calls == [{"profile": profile, "operation_id": "createQuery"}]
@@ -1,13 +1,12 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import UTC, datetime
from hashlib import sha256
from http import HTTPStatus
from inspect import unwrap
from types import SimpleNamespace
from typing import Literal
from unittest.mock import MagicMock
from typing import Literal, cast
import pytest
from flask import Flask
@@ -43,19 +42,30 @@ from controllers.service_api.knowledge_fs.error import (
KnowledgeFSServiceUpstreamUnavailableHTTPError,
)
from libs.exception import BaseHTTPException
from models import Account
from models.knowledge_fs import KnowledgeFSAppSpaceJoinType
from services.knowledge_fs.capability_broker import KnowledgeFSIssuedProductCapability
from services.knowledge_fs.product_dto import (
KnowledgeFSOverviewBaseStatsResponse,
KnowledgeFSOverviewQueryOutcomesResponse,
KnowledgeFSPublicFailureResponse,
KnowledgeFSQueryCreatePayload,
KnowledgeFSSourceResponse,
KnowledgeFSSourceSyncPolicyResponse,
KnowledgeFSSourceUpdatePayload,
KnowledgeFSSourceWorkflowResponse,
KnowledgeFSStreamCapabilityPayload,
)
from services.knowledge_fs.product_remote import KnowledgeFSOperationUnavailableError
from services.knowledge_fs.runtime import KnowledgeFSRuntime
from tests.unit_tests.config_override import apply_config_overrides
from tests.unit_tests.controllers.knowledge_fs_controller_fakes import (
FakeComponent,
FakeRuntime,
Recorder,
invocation,
raising,
)
_RAW_RESULT = object()
@@ -79,6 +89,52 @@ def source_response(**overrides: object) -> KnowledgeFSSourceResponse:
)
def sync_policy_response(revision: int) -> KnowledgeFSSourceSyncPolicyResponse:
return KnowledgeFSSourceSyncPolicyResponse.model_validate(
{
"created_at": datetime(2026, 1, 1, tzinfo=UTC),
"enabled": True,
"expected_source_version": 1,
"id": "policy-1",
"knowledge_space_id": "space-1",
"mode": "interval",
"revision": revision,
"source_id": "source-1",
"updated_at": datetime(2026, 1, 1, tzinfo=UTC),
}
)
def issued_capability() -> KnowledgeFSIssuedProductCapability:
return KnowledgeFSIssuedProductCapability(
token="capability-token",
expires_at=datetime(2026, 7, 21, tzinfo=UTC),
operation_id="createQuery",
knowledge_space_id="knowledge-space-1",
knowledge_space_revision=1,
trace_id="trace-1",
)
def _account(account_id: str) -> Account:
account = Account(name="Test User", email="user@example.com")
account.id = account_id
return account
@dataclass(frozen=True)
class _SettingsUpdateResult:
"""The only field the settings route reads before dumping the facade result."""
migration: None = None
@dataclass(frozen=True)
class _MembersPayload:
members: tuple[str, ...]
query_images: list[str] = field(default_factory=list)
def _invoke[R](resource_type: type[Resource], handler: Callable[..., R], *args: object) -> R:
return unwrap(handler)(resource_type(), *args)
@@ -674,23 +730,18 @@ def test_console_resources_delegate_one_tenant_scoped_product_operation(
payload = (
KnowledgeFSSourceUpdatePayload(name="Renamed")
if class_name.__name__ == "KnowledgeFSSpaceSourceApi" and method_name.__name__ == "patch"
else SimpleNamespace(members=("member-1",), query_images=[])
else _MembersPayload(members=("member-1",))
)
runtime = SimpleNamespace(
application=MagicMock(),
control_plane=MagicMock(),
app_bindings=MagicMock(),
facade=MagicMock(),
delegate = Recorder(_RAW_RESULT)
components = {name: FakeComponent() for name in ("application", "control_plane", "app_bindings", "facade")}
components[component_name] = FakeComponent(**{delegate_name: delegate})
runtime = FakeRuntime(
application=components["application"],
control_plane=components["control_plane"],
app_bindings=components["app_bindings"],
facade=components["facade"],
)
component = {
"application": runtime.application,
"control_plane": runtime.control_plane,
"app_bindings": runtime.app_bindings,
"facade": runtime.facade,
}[component_name]
delegate = MagicMock(return_value=_RAW_RESULT)
component.configure_mock(**{delegate_name: delegate})
dump_response = MagicMock(side_effect=lambda schema, raw: (schema.__name__, raw))
dump_response = Recorder(effect=lambda schema, raw: (schema.__name__, raw))
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
monkeypatch.setattr(console_resources, "_payload", lambda _: payload)
@@ -702,8 +753,8 @@ def test_console_resources_delegate_one_tenant_scoped_product_operation(
with app.test_request_context("/", method="POST"):
result = _invoke(class_name, method_name, *route_args)
delegate.assert_called_once()
call_fields = delegate.call_args.kwargs
assert len(delegate.calls) == 1
call_fields = delegate.last_call.kwargs
assert call_fields["tenant_id"] == "tenant-1"
assert call_fields.get("account_id", call_fields.get("actor_account_id")) == "account-1"
for field_name, expected in expected_fields.items():
@@ -717,16 +768,15 @@ def test_console_resources_delegate_one_tenant_scoped_product_operation(
assert call_fields["members"] == ("member-1",)
if "idempotency_key" in call_fields:
assert call_fields["idempotency_key"] == "idempotency-1"
if dump_response.called:
assert dump_response.call_args.args[1] is _RAW_RESULT
if dump_response.calls:
assert dump_response.last_call.args[1] is _RAW_RESULT
else:
assert result == ("", HTTPStatus.NO_CONTENT)
def test_console_document_reference_delegates_exact_asset_identity(monkeypatch: pytest.MonkeyPatch) -> None:
facade = MagicMock()
facade.resolve_document_reference.return_value = _RAW_RESULT
runtime = SimpleNamespace(facade=facade)
facade = FakeComponent(resolve_document_reference=Recorder(_RAW_RESULT))
runtime = FakeRuntime(facade=facade)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
monkeypatch.setattr(console_resources, "dump_response", lambda _schema, raw: raw)
@@ -740,19 +790,20 @@ def test_console_document_reference_delegates_exact_asset_identity(monkeypatch:
)
assert result is _RAW_RESULT
facade.resolve_document_reference.assert_called_once_with(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="space-1",
document_asset_id="asset-1",
document_asset_version=7,
)
assert facade.resolve_document_reference.calls == [
invocation(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="space-1",
document_asset_id="asset-1",
document_asset_version=7,
)
]
def test_console_space_list_preserves_repeated_creator_filters(monkeypatch: pytest.MonkeyPatch) -> None:
application = MagicMock()
application.list_spaces.return_value = _RAW_RESULT
runtime = SimpleNamespace(application=application)
application = FakeComponent(list_spaces=Recorder(_RAW_RESULT))
runtime = FakeRuntime(application=application)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
monkeypatch.setattr(console_resources, "dump_response", lambda _schema, raw: raw)
@@ -763,24 +814,25 @@ def test_console_space_list_preserves_repeated_creator_filters(monkeypatch: pyte
):
result = _invoke(console_resources.KnowledgeFSSpacesApi, console_resources.KnowledgeFSSpacesApi.get)
application.list_spaces.assert_called_once_with(
tenant_id="tenant-1",
account_id="account-1",
page=2,
limit=10,
creator_ids=["creator-1", "creator-2"],
tag_ids=["tag-1", "tag-2"],
query="Support",
)
assert application.list_spaces.calls == [
invocation(
tenant_id="tenant-1",
account_id="account-1",
page=2,
limit=10,
creator_ids=["creator-1", "creator-2"],
tag_ids=["tag-1", "tag-2"],
query="Support",
)
]
assert result is _RAW_RESULT
def test_console_source_update_does_not_start_sync_before_resource_selection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
facade = MagicMock()
facade.update_source.return_value = _RAW_RESULT
runtime = SimpleNamespace(facade=facade)
facade = FakeComponent(update_source=Recorder(_RAW_RESULT))
runtime = FakeRuntime(facade=facade)
payload = KnowledgeFSSourceUpdatePayload.model_validate(
{
"expectedVersion": 3,
@@ -802,7 +854,7 @@ def test_console_source_update_does_not_start_sync_before_resource_selection(
"source-1",
)
forwarded = facade.update_source.call_args.kwargs["payload"]
forwarded = facade.update_source.last_call.kwargs["payload"]
assert forwarded is not payload
assert forwarded.status == "disabled"
assert forwarded.sync_after_update is None
@@ -844,8 +896,6 @@ def test_console_source_update_commits_complete_edit_selection(
selection: dict[str, object],
expected_import_kind: str,
) -> None:
facade = MagicMock()
facade.update_source.return_value = SimpleNamespace(version=4)
selection_kind = selection["kind"]
assert isinstance(selection_kind, str)
provider_kind = {"online_document": "online-document", "online_drive": "online-drive"}.get(selection_kind)
@@ -854,10 +904,28 @@ def test_console_source_update_commits_complete_edit_selection(
type="web" if selection_kind == "website_crawl" else "connector",
uri="https://old.example.com",
)
updated = SimpleNamespace(sync_workflow=None)
facade.get_source.side_effect = [original, updated]
workflow = SimpleNamespace(id="workflow-1")
runtime = SimpleNamespace(facade=facade)
updated = source_response(sync_workflow=None)
facade = FakeComponent(
get_source=Recorder(results=[original, updated]),
update_source=Recorder(source_response(version=4)),
)
workflow = KnowledgeFSSourceWorkflowResponse.model_validate(
{
"checkpoint": "queued",
"created_at": datetime(2026, 1, 1, tzinfo=UTC),
"execution_attempts": 0,
"id": "workflow-1",
"knowledge_space_id": "space-1",
"kind": "source-import",
"max_execution_attempts": 3,
"progress_completed": 0,
"progress_failed": 0,
"progress_skipped": 0,
"state": "queued",
"updated_at": datetime(2026, 1, 1, tzinfo=UTC),
}
)
runtime = FakeRuntime(facade=facade)
payload = KnowledgeFSSourceUpdatePayload.model_validate(
{
"expectedVersion": 3,
@@ -866,7 +934,7 @@ def test_console_source_update_commits_complete_edit_selection(
"syncPolicy": {"enabled": True, "mode": "interval"},
}
)
commit = MagicMock(return_value=workflow)
commit = Recorder(workflow)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
monkeypatch.setattr(console_resources, "_payload", lambda _: payload)
@@ -882,12 +950,12 @@ def test_console_source_update_commits_complete_edit_selection(
"source-1",
)
facade.update_source.assert_not_called()
source_payload = commit.call_args.kwargs["source_update"]
assert facade.update_source.calls == []
source_payload = commit.last_call.kwargs["source_update"]
assert source_payload.status == "disabled"
import_payload = commit.call_args.kwargs["payload"]
import_payload = commit.last_call.kwargs["payload"]
assert import_payload.kind == expected_import_kind
assert commit.call_args.kwargs["idempotency_key"] == "source-edit:source-1:3"
assert commit.last_call.kwargs["idempotency_key"] == "source-edit:source-1:3"
assert updated.sync_workflow is workflow
assert result is updated
@@ -895,7 +963,6 @@ def test_console_source_update_commits_complete_edit_selection(
def test_console_source_update_does_not_import_an_unchanged_complete_selection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
facade = MagicMock()
source = source_response(
metadata={"crawled": {"https://example.com/a": {}}},
sync_policy=None,
@@ -903,17 +970,20 @@ def test_console_source_update_does_not_import_an_unchanged_complete_selection(
uri="https://example.com",
version=3,
)
facade.get_source.return_value = source
facade.get_source_sync_policy.return_value = SimpleNamespace(revision=2)
facade.update_source.return_value = source
runtime = SimpleNamespace(facade=facade)
facade = FakeComponent(
get_source=Recorder(source),
get_source_sync_policy=Recorder(sync_policy_response(revision=2)),
update_source=Recorder(source),
update_source_sync_policy=Recorder(),
)
runtime = FakeRuntime(facade=facade)
payload = KnowledgeFSSourceUpdatePayload.model_validate(
{
"selection": {"kind": "website_crawl", "sourceUrls": ["https://example.com/a"]},
"syncPolicy": {"enabled": True, "mode": "interval"},
}
)
commit = MagicMock()
commit = Recorder()
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
monkeypatch.setattr(console_resources, "_payload", lambda _: payload)
@@ -929,10 +999,10 @@ def test_console_source_update_does_not_import_an_unchanged_complete_selection(
"source-1",
)
facade.update_source.assert_called_once()
commit.assert_not_called()
facade.update_source_sync_policy.assert_not_called()
command = facade.update_source.call_args.kwargs["payload"]
assert len(facade.update_source.calls) == 1
assert commit.calls == []
assert facade.update_source_sync_policy.calls == []
command = facade.update_source.last_call.kwargs["payload"]
assert command.expected_version == 3
assert command.expected_policy_revision == 2
assert command.sync_policy.mode == "interval"
@@ -1062,18 +1132,20 @@ def test_source_edit_reimports_an_unchanged_selection_when_source_is_in_error()
def test_console_source_update_applies_policy_without_creating_import_work(
monkeypatch: pytest.MonkeyPatch,
) -> None:
facade = MagicMock()
source = source_response(version=5, sync_policy=None)
facade.get_source.return_value = source
facade.get_source_sync_policy.return_value = SimpleNamespace(revision=2)
policy = SimpleNamespace(revision=3)
policy = sync_policy_response(revision=3)
source.sync_policy = policy
facade.update_source.return_value = source
runtime = SimpleNamespace(facade=facade)
facade = FakeComponent(
get_source=Recorder(source),
get_source_sync_policy=Recorder(sync_policy_response(revision=2)),
update_source=Recorder(source),
update_source_sync_policy=Recorder(),
)
runtime = FakeRuntime(facade=facade)
payload = KnowledgeFSSourceUpdatePayload.model_validate(
{"syncPolicy": {"customIntervalSeconds": 7200, "enabled": True, "mode": "custom"}}
)
commit = MagicMock()
commit = Recorder()
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
monkeypatch.setattr(console_resources, "_payload", lambda _: payload)
@@ -1089,10 +1161,10 @@ def test_console_source_update_applies_policy_without_creating_import_work(
"source-1",
)
facade.update_source.assert_called_once()
commit.assert_not_called()
facade.update_source_sync_policy.assert_not_called()
policy_payload = facade.update_source.call_args.kwargs["payload"]
assert len(facade.update_source.calls) == 1
assert commit.calls == []
assert facade.update_source_sync_policy.calls == []
policy_payload = facade.update_source.last_call.kwargs["payload"]
assert policy_payload.expected_policy_revision == 2
assert policy_payload.expected_version == 5
assert policy_payload.sync_policy.custom_interval_seconds == 7200
@@ -1101,9 +1173,8 @@ def test_console_source_update_applies_policy_without_creating_import_work(
def test_console_metadata_delete_forwards_row_version_cas(monkeypatch: pytest.MonkeyPatch) -> None:
facade = MagicMock()
facade.delete_metadata_field.return_value = _RAW_RESULT
runtime = SimpleNamespace(facade=facade)
facade = FakeComponent(delete_metadata_field=Recorder(_RAW_RESULT))
runtime = FakeRuntime(facade=facade)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
monkeypatch.setattr(console_resources, "dump_response", lambda _schema, raw: raw)
@@ -1117,13 +1188,15 @@ def test_console_metadata_delete_forwards_row_version_cas(monkeypatch: pytest.Mo
"field-1",
)
facade.delete_metadata_field.assert_called_once_with(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="space-1",
field_id="field-1",
expected_row_version=7,
)
assert facade.delete_metadata_field.calls == [
invocation(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="space-1",
field_id="field-1",
expected_row_version=7,
)
]
assert result is _RAW_RESULT
@@ -1188,14 +1261,14 @@ def test_console_overview_stats_composes_kfs_metrics_with_dify_app_bindings(
"window": "7d",
}
)
runtime = SimpleNamespace(
facade=SimpleNamespace(
get_overview_query_outcomes=MagicMock(return_value=outcomes),
get_overview_stats=MagicMock(return_value=stats),
runtime = FakeRuntime(
facade=FakeComponent(
get_overview_query_outcomes=Recorder(outcomes),
get_overview_stats=Recorder(stats),
),
app_bindings=SimpleNamespace(count_active=MagicMock(return_value=7)),
app_bindings=FakeComponent(count_active=Recorder(7)),
)
dump_response = MagicMock(side_effect=lambda _schema, raw: raw)
dump_response = Recorder(effect=lambda _schema, raw: raw)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: runtime)
monkeypatch.setattr(console_resources, "dump_response", dump_response)
@@ -1208,22 +1281,28 @@ def test_console_overview_stats_composes_kfs_metrics_with_dify_app_bindings(
"control-space-1",
)
runtime.facade.get_overview_stats.assert_called_once_with(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-space-1",
)
runtime.facade.get_overview_query_outcomes.assert_called_once_with(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-space-1",
window="7d",
)
runtime.app_bindings.count_active.assert_called_once_with(
tenant_id="tenant-1",
actor_account_id="account-1",
control_space_id="control-space-1",
)
assert runtime.facade.get_overview_stats.calls == [
invocation(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-space-1",
)
]
assert runtime.facade.get_overview_query_outcomes.calls == [
invocation(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-space-1",
window="7d",
)
]
assert runtime.app_bindings.count_active.calls == [
invocation(
tenant_id="tenant-1",
actor_account_id="account-1",
control_space_id="control-space-1",
)
]
assert result.documents == 13
assert result.linked_apps == 7
assert result.queries.value == 120
@@ -1233,7 +1312,7 @@ def test_console_overview_stats_composes_kfs_metrics_with_dify_app_bindings(
assert result.answer_rate.change_percentage_points == pytest.approx(-5)
assert result.freshness_seconds == 3600
assert result.generated_at == outcomes.generated_at
assert dump_response.call_args.args[1] is result
assert dump_response.last_call.args[1] is result
_SERVICE_DELEGATION_CASES = (
@@ -1476,8 +1555,8 @@ def test_service_resources_bind_route_identifiers_to_one_declared_operation(
expected_fields: dict[str, object],
) -> None:
payload = object()
execute = MagicMock(return_value=_RAW_RESULT)
dump_response = MagicMock(side_effect=lambda schema, raw: (schema.__name__, raw))
execute = Recorder(_RAW_RESULT)
dump_response = Recorder(effect=lambda schema, raw: (schema.__name__, raw))
monkeypatch.setattr(service_resources, "_execute_service_operation", execute)
monkeypatch.setattr(service_resources, "_payload", lambda _: payload)
monkeypatch.setattr(service_resources, "_idempotency_key", lambda: "idempotency-1")
@@ -1500,8 +1579,8 @@ def test_service_resources_bind_route_identifiers_to_one_declared_operation(
with app.test_request_context("/", method="POST", headers={"Idempotency-Key": "idempotency-1"}):
_invoke(class_name, method_name, *route_args)
execute.assert_called_once()
call_fields = execute.call_args.kwargs
assert len(execute.calls) == 1
call_fields = execute.last_call.kwargs
assert call_fields["control_space_id"] == "space-1"
assert call_fields["operation_id"] == operation_id
for field_name, expected in expected_fields.items():
@@ -1512,7 +1591,7 @@ def test_service_resources_bind_route_identifiers_to_one_declared_operation(
assert call_fields["headers"] == (("Idempotency-Key", "idempotency-1"),)
if "query" in call_fields:
assert call_fields["query"] == (("normalized", "true"),)
assert dump_response.call_args.args[1] is _RAW_RESULT
assert dump_response.last_call.args[1] is _RAW_RESULT
@pytest.mark.parametrize(
@@ -1563,13 +1642,13 @@ def test_service_credential_routes_validate_profile_before_facade_delegation(
operation_id: str,
cursor: str | None,
) -> None:
facade = SimpleNamespace(
execute_service=MagicMock(return_value=_RAW_RESULT),
update_service_settings=MagicMock(return_value=SimpleNamespace(migration=None)),
facade = FakeComponent(
execute_service=Recorder(_RAW_RESULT),
update_service_settings=Recorder(_SettingsUpdateResult()),
)
runtime = SimpleNamespace(facade=facade)
runtime = FakeRuntime(facade=facade)
profile = object()
validate_profile = MagicMock(return_value=profile)
validate_profile = Recorder(profile)
monkeypatch.setattr(service_resources, "_runtime", lambda: runtime)
monkeypatch.setattr(service_resources, "_profile", validate_profile)
monkeypatch.setattr(service_resources, "_payload", lambda _: object())
@@ -1580,16 +1659,16 @@ def test_service_credential_routes_validate_profile_before_facade_delegation(
with app.test_request_context("/", method="POST", query_string=query_string):
_invoke(class_name, method_name, "space-1")
validate_profile.assert_called_once_with(runtime, operation_id=operation_id, control_space_id="space-1")
assert validate_profile.calls == [invocation(runtime, operation_id=operation_id, control_space_id="space-1")]
if operation_id == "updateSettings":
assert facade.update_service_settings.call_args.kwargs["profile"] is profile
facade.execute_service.assert_not_called()
assert facade.update_service_settings.last_call.kwargs["profile"] is profile
assert facade.execute_service.calls == []
return
assert facade.execute_service.call_args.kwargs["profile"] is profile
assert facade.execute_service.call_args.kwargs["operation_id"] == operation_id
if "query" in facade.execute_service.call_args.kwargs:
assert facade.execute_service.last_call.kwargs["profile"] is profile
assert facade.execute_service.last_call.kwargs["operation_id"] == operation_id
if "query" in facade.execute_service.last_call.kwargs:
expected_query = (("limit", "50"),) + ((("cursor", cursor),) if cursor else ())
assert facade.execute_service.call_args.kwargs["query"] == expected_query
assert facade.execute_service.last_call.kwargs["query"] == expected_query
@pytest.mark.parametrize(
@@ -1604,13 +1683,7 @@ def test_deprecated_buffered_routes_fail_closed(class_name: type[console_resourc
def test_console_stream_capabilities_bind_the_authorized_resource(monkeypatch: pytest.MonkeyPatch) -> None:
issued = SimpleNamespace(
token="capability-token",
trace_id="trace-1",
expires_at=datetime(2026, 7, 21, tzinfo=UTC),
knowledge_space_id="knowledge-space-1",
)
broker = SimpleNamespace(issue_interactive=MagicMock(return_value=issued))
broker = FakeComponent(issue_interactive=Recorder(issued_capability()))
payloads = iter(
[
KnowledgeFSQueryCreatePayload(query="question", mode="deep"),
@@ -1622,7 +1695,7 @@ def test_console_stream_capabilities_bind_the_authorized_resource(monkeypatch: p
monkeypatch.setattr(
console_resources,
"_console_services",
lambda: SimpleNamespace(broker=broker),
lambda: FakeRuntime(broker=broker),
)
monkeypatch.setattr(console_resources, "_payload", lambda _: next(payloads))
monkeypatch.setattr(console_resources, "dump_response", lambda _, response: response)
@@ -1652,26 +1725,20 @@ def test_console_stream_capabilities_bind_the_authorized_resource(monkeypatch: p
"?knowledgeSpaceId=knowledge-space-1"
)
assert legacy_query.url == "https://dify.example/console/api/knowledge-fs/query-stream"
assert broker.issue_interactive.call_args_list[0].kwargs["operation_id"] == "createQuery"
assert broker.issue_interactive.call_args_list[1].kwargs == {
assert broker.issue_interactive.calls[0].kwargs["operation_id"] == "createQuery"
assert broker.issue_interactive.calls[1].kwargs == {
"tenant_id": "tenant-1",
"account_id": "account-1",
"control_space_id": "space-2",
"operation_id": "streamResearchTask",
"resource_id": "task/1",
}
assert broker.issue_interactive.call_args_list[2].kwargs["operation_id"] == "createQuery"
assert broker.issue_interactive.calls[2].kwargs["operation_id"] == "createQuery"
def test_service_query_admission_binds_profile_space_and_payload(monkeypatch: pytest.MonkeyPatch) -> None:
issued = SimpleNamespace(
token="capability-token",
trace_id="trace-1",
expires_at=datetime(2026, 7, 21, tzinfo=UTC),
knowledge_space_id="knowledge-space-1",
)
broker = SimpleNamespace(issue_service=MagicMock(return_value=issued))
runtime = SimpleNamespace(broker=broker)
broker = FakeComponent(issue_service=Recorder(issued_capability()))
runtime = FakeRuntime(broker=broker)
profile = object()
apply_config_overrides(monkeypatch, SERVICE_API_URL="https://api.dify.example")
monkeypatch.setattr(service_resources, "_runtime", lambda: runtime)
@@ -1687,7 +1754,7 @@ def test_service_query_admission_binds_profile_space_and_payload(monkeypatch: py
"space-1",
)
broker.issue_service.assert_called_once_with(profile=profile, operation_id="createQuery")
assert broker.issue_service.calls == [invocation(profile=profile, operation_id="createQuery")]
assert response.request.knowledge_space_id == "knowledge-space-1"
assert response.request.query == "question"
assert response.url == "https://api.dify.example/v1/knowledge-fs/query-stream"
@@ -1696,21 +1763,21 @@ def test_service_query_admission_binds_profile_space_and_payload(monkeypatch: py
def test_console_resource_helpers_validate_feature_payload_headers_and_query_pairs(
monkeypatch: pytest.MonkeyPatch,
) -> None:
runtime = object()
runtime = FakeRuntime()
session_maker = object()
get_runtime = MagicMock(return_value=runtime)
get_runtime = Recorder(runtime)
apply_config_overrides(monkeypatch, KNOWLEDGE_FS_ENABLED=True)
monkeypatch.setattr(console_resources.session_factory, "get_session_maker", lambda: session_maker)
monkeypatch.setattr(console_resources, "get_knowledge_fs_runtime", get_runtime)
monkeypatch.setattr(
console_resources,
"current_account_with_tenant",
lambda: (SimpleNamespace(id="account-1"), "tenant-1"),
lambda: (_account("account-1"), "tenant-1"),
)
app = Flask(__name__)
assert console_resources._console_services() is runtime
get_runtime.assert_called_once_with(session_maker)
assert get_runtime.calls == [invocation(session_maker)]
assert console_resources._actor() == ("account-1", "tenant-1")
query = KnowledgeFSQueryCreatePayload(query="question", mode="fast", active_document_ids=["doc-1"])
assert console_resources._query_pairs(query) == (
@@ -1736,16 +1803,16 @@ def test_console_resource_helpers_validate_feature_payload_headers_and_query_pai
def test_service_resource_helpers_validate_feature_bearer_headers_and_boolean_queries(
monkeypatch: pytest.MonkeyPatch,
) -> None:
runtime = object()
runtime = FakeRuntime()
session_maker = object()
get_runtime = MagicMock(return_value=runtime)
get_runtime = Recorder(runtime)
apply_config_overrides(monkeypatch, KNOWLEDGE_FS_ENABLED=True)
monkeypatch.setattr(service_resources.session_factory, "get_session_maker", lambda: session_maker)
monkeypatch.setattr(service_resources, "get_knowledge_fs_runtime", get_runtime)
app = Flask(__name__)
assert service_resources._runtime() is runtime
get_runtime.assert_called_once_with(session_maker)
assert get_runtime.calls == [invocation(session_maker)]
class Query(BaseModel):
enabled: bool = True
@@ -1786,12 +1853,12 @@ def test_console_request_rejections_preserve_http_status_contracts() -> None:
504: KnowledgeFSTimeoutHTTPError,
}
for status, http_error in expected.items():
reject = console_resources._knowledge_fs_errors(
MagicMock(side_effect=KnowledgeFSProductRequestRejectedError(status_code=status))
guarded = console_resources._knowledge_fs_errors(
raising(KnowledgeFSProductRequestRejectedError(status_code=status))
)
with pytest.raises(http_error) as raised:
reject()
guarded()
assert raised.value.code == status
assert raised.value.data == {
@@ -1836,13 +1903,13 @@ def test_console_unavailable_and_timeout_rejections_preserve_safe_failure_metada
failure = KnowledgeFSPublicFailureResponse.model_validate(
{**expected_failure, "message": "Private upstream diagnostic that must not reach the client"}
)
reject = console_resources._knowledge_fs_errors(
MagicMock(side_effect=KnowledgeFSProductRequestRejectedError(status_code=status, failure=failure))
guarded = console_resources._knowledge_fs_errors(
raising(KnowledgeFSProductRequestRejectedError(status_code=status, failure=failure))
)
http_error = KnowledgeFSOperationUnavailableHTTPError if status == 503 else KnowledgeFSTimeoutHTTPError
with pytest.raises(http_error) as raised:
reject()
guarded()
assert raised.value.code == status
assert raised.value.data == {
@@ -1866,12 +1933,12 @@ def test_console_request_rejection_preserves_safe_failure_metadata() -> None:
"traceId": "trace-model",
}
)
reject = console_resources._knowledge_fs_errors(
MagicMock(side_effect=KnowledgeFSProductRequestRejectedError(status_code=422, failure=failure))
guarded = console_resources._knowledge_fs_errors(
raising(KnowledgeFSProductRequestRejectedError(status_code=422, failure=failure))
)
with pytest.raises(KnowledgeFSRequestRejectedHTTPError) as raised:
reject()
guarded()
assert raised.value.data == {
"code": "knowledge_fs_request_rejected",
@@ -1940,10 +2007,10 @@ def test_console_error_adapter_maps_every_domain_boundary_to_the_stable_http_con
)
for domain_error, http_error in mappings:
fail = console_resources._knowledge_fs_errors(MagicMock(side_effect=domain_error))
guarded = console_resources._knowledge_fs_errors(raising(domain_error))
with pytest.raises(http_error):
fail()
guarded()
assert KnowledgeFSUpstreamUnavailableHTTPError.code == HTTPStatus.SERVICE_UNAVAILABLE
@@ -1973,10 +2040,10 @@ def test_service_error_adapter_maps_every_domain_boundary_to_the_stable_http_con
)
for domain_error, http_error in mappings:
fail = service_resources._service_api_errors(MagicMock(side_effect=domain_error))
guarded = service_resources._service_api_errors(raising(domain_error))
with pytest.raises(http_error):
fail()
guarded()
rejected_mappings = (
(400, KnowledgeFSServiceInvalidRequestHTTPError),
@@ -1987,11 +2054,11 @@ def test_service_error_adapter_maps_every_domain_boundary_to_the_stable_http_con
(429, KnowledgeFSServiceRateLimitHTTPError),
)
for status, http_error in rejected_mappings:
fail = service_resources._service_api_errors(
MagicMock(side_effect=KnowledgeFSProductRequestRejectedError(status_code=status))
guarded = service_resources._service_api_errors(
raising(KnowledgeFSProductRequestRejectedError(status_code=status))
)
with pytest.raises(http_error):
fail()
guarded()
def test_console_small_file_reader_rejects_malformed_and_empty_multipart_bodies() -> None:
@@ -2027,12 +2094,12 @@ def test_console_app_binding_route_rejects_unknown_caller_kind_before_revoke(
) -> None:
from services.knowledge_fs.app_binding_management import KnowledgeFSAppBindingManagementError
revoke = MagicMock()
revoke = Recorder()
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(
console_resources,
"_console_services",
lambda: SimpleNamespace(app_bindings=SimpleNamespace(revoke=revoke)),
lambda: FakeRuntime(app_bindings=FakeComponent(revoke=revoke)),
)
with pytest.raises(KnowledgeFSAppBindingManagementError, match="caller kind"):
@@ -2044,15 +2111,16 @@ def test_console_app_binding_route_rejects_unknown_caller_kind_before_revoke(
"app-1",
)
revoke.assert_not_called()
assert revoke.calls == []
def test_service_profile_hides_unknown_operations_before_dataset_key_authorization(
monkeypatch: pytest.MonkeyPatch,
) -> None:
authorization = SimpleNamespace(authorize=MagicMock())
runtime = MagicMock(spec=KnowledgeFSRuntime, service_api_authorization=authorization)
monkeypatch.setattr(service_resources, "product_operation_action", MagicMock(side_effect=KeyError("unknown")))
authorization = FakeComponent(authorize=Recorder())
# The profile check must fail before it touches any runtime component but authorization.
runtime = cast(KnowledgeFSRuntime, FakeRuntime(service_api_authorization=authorization))
monkeypatch.setattr(service_resources, "product_operation_action", raising(KeyError("unknown")))
with pytest.raises(KnowledgeFSOperationUnavailableError, match="unknownOperation"):
service_resources._profile(
@@ -2061,7 +2129,7 @@ def test_service_profile_hides_unknown_operations_before_dataset_key_authorizati
control_space_id="space-1",
)
authorization.authorize.assert_not_called()
assert authorization.authorize.calls == []
def test_service_operation_helper_forwards_the_complete_validated_request(
@@ -2069,10 +2137,10 @@ def test_service_operation_helper_forwards_the_complete_validated_request(
) -> None:
profile = object()
payload = KnowledgeFSQueryCreatePayload(query="question")
facade = SimpleNamespace(execute_service=MagicMock(return_value={"id": "result-1"}))
runtime = SimpleNamespace(facade=facade)
facade = FakeComponent(execute_service=Recorder({"id": "result-1"}))
runtime = FakeRuntime(facade=facade)
monkeypatch.setattr(service_resources, "_runtime", lambda: runtime)
monkeypatch.setattr(service_resources, "_profile", MagicMock(return_value=profile))
monkeypatch.setattr(service_resources, "_profile", Recorder(profile))
result = service_resources._execute_service_operation(
control_space_id="space-1",
@@ -2086,13 +2154,15 @@ def test_service_operation_helper_forwards_the_complete_validated_request(
)
assert result == {"id": "result-1"}
facade.execute_service.assert_called_once_with(
profile=profile,
operation_id="createResearchTask",
payload=payload,
query=(("cursor", "next"),),
bind_space_in_body=True,
resource_id="task-1",
path_parameters=(("taskId", "task-1"),),
headers=(("Idempotency-Key", "request-key"),),
)
assert facade.execute_service.calls == [
invocation(
profile=profile,
operation_id="createResearchTask",
payload=payload,
query=(("cursor", "next"),),
bind_space_in_body=True,
resource_id="task-1",
path_parameters=(("taskId", "task-1"),),
headers=(("Idempotency-Key", "request-key"),),
)
]
@@ -1,8 +1,5 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from flask import Flask
from pydantic import ValidationError
@@ -24,7 +21,9 @@ from services.knowledge_fs.product_dto import (
KnowledgeFSServiceSourceCreatePayload,
KnowledgeFSServiceSourceUpdatePayload,
)
from services.knowledge_fs.service_api_authorization import KnowledgeFSServiceApiProfile
from tests.unit_tests.config_override import apply_config_overrides
from tests.unit_tests.controllers.knowledge_fs_controller_fakes import FakeComponent, FakeRuntime, Recorder, invocation
_NOW = "2026-09-17T00:00:00Z"
_FAILURE = {
@@ -68,10 +67,21 @@ def _workflow() -> dict[str, object]:
@pytest.fixture
def runtime(monkeypatch: pytest.MonkeyPatch):
facade = SimpleNamespace(execute_service=MagicMock())
value = SimpleNamespace(facade=facade)
value = FakeRuntime(facade=FakeComponent(execute_service=Recorder()))
profile = KnowledgeFSServiceApiProfile(
tenant_id="tenant-1",
control_space_id="control-1",
api_token_id="token-1",
principal_id="principal-1",
knowledge_space_id="space-1",
knowledge_space_revision=1,
membership_epoch=1,
space_acl_epoch=1,
external_access_epoch=1,
content_policy_revision=1,
)
monkeypatch.setattr(resources, "_runtime", lambda: value)
monkeypatch.setattr(resources, "_profile", lambda *_args, **_kwargs: SimpleNamespace(tenant_id="tenant-1"))
monkeypatch.setattr(resources, "_profile", lambda *_args, **_kwargs: profile)
return value
@@ -183,7 +193,7 @@ def test_logical_read_exposes_current_cas_and_pagination(runtime) -> None:
result = resources.KnowledgeFSServiceLogicalDocumentsApi().get("control-1")
assert result["data"][0]["row_version"] == 7
assert result["data"][0]["id"] == "doc-1"
assert runtime.facade.execute_service.call_args.kwargs["query"] == (("limit", "17"), ("cursor", "prev"))
assert runtime.facade.execute_service.last_call.kwargs["query"] == (("limit", "17"), ("cursor", "prev"))
def test_deletion_links_and_partial_batch_results_stay_on_public_origin(
@@ -238,7 +248,7 @@ def test_legacy_source_import_adapts_coordinates_and_admits_durable_workflow(run
result, status = resource.post("control-1", "source-1")
assert status == 202
assert result["id"] == "run-1"
kwargs = runtime.facade.execute_service.call_args.kwargs
kwargs = runtime.facade.execute_service.last_call.kwargs
assert kwargs["operation_id"] == "importSourceWorkflow"
assert kwargs["headers"] == (("Idempotency-Key", "import-once"),)
assert kwargs["payload"].model_dump(mode="json", by_alias=True)["items"][0]["providerItemId"] == coordinate
@@ -249,12 +259,12 @@ def test_malformed_upstream_is_502_and_invalid_input_is_400_without_input_echo(r
with Flask(__name__).test_request_context("/"):
with pytest.raises(KnowledgeFSServiceUpstreamUnavailableHTTPError):
resources.KnowledgeFSServiceLogicalDocumentApi().get("control-1", "doc-1")
runtime.facade.execute_service.reset_mock()
runtime.facade.execute_service.calls.clear()
with Flask(__name__).test_request_context("/?limit=private-invalid-value"):
with pytest.raises(KnowledgeFSServiceInvalidRequestHTTPError) as caught:
resources.KnowledgeFSServiceLogicalDocumentsApi().get("control-1")
assert "private-invalid-value" not in str(caught.value.data)
runtime.facade.execute_service.assert_not_called()
assert runtime.facade.execute_service.calls == []
def test_resume_binds_original_task_and_processing_history_preserves_superseded(runtime) -> None:
@@ -276,7 +286,7 @@ def test_resume_binds_original_task_and_processing_history_preserves_superseded(
with Flask(__name__).test_request_context("/"):
response = resources.KnowledgeFSServiceDocumentProcessingTasksApi().get("control-1", "doc-1")
assert response["data"][0]["state"] == "superseded"
assert runtime.facade.execute_service.call_args.kwargs["resource_id"] == "doc-1"
assert runtime.facade.execute_service.last_call.kwargs["resource_id"] == "doc-1"
runtime.facade.execute_service.return_value = {
"id": "task-1",
"knowledgeSpaceId": "space-1",
@@ -290,8 +300,8 @@ def test_resume_binds_original_task_and_processing_history_preserves_superseded(
with Flask(__name__).test_request_context("/", method="POST"):
response = resources.KnowledgeFSServiceResearchTaskResumeApi().post("control-1", "task-1")
assert response["id"] == "task-1"
assert runtime.facade.execute_service.call_args.kwargs["operation_id"] == "resumeResearchTask"
assert runtime.facade.execute_service.call_args.kwargs["resource_id"] == "task-1"
assert runtime.facade.execute_service.last_call.kwargs["operation_id"] == "resumeResearchTask"
assert runtime.facade.execute_service.last_call.kwargs["resource_id"] == "task-1"
@pytest.mark.parametrize("status", [503, 504])
@@ -315,21 +325,21 @@ def test_service_preserves_temporary_failure_status_and_safe_violations(status:
def test_service_image_upload_uses_authenticated_profile_and_typed_response(monkeypatch: pytest.MonkeyPatch) -> None:
import io
upload = MagicMock(return_value={"upload_file_id": "image-1", "byte_size": 8, "mime_type": "image/png"})
upload = Recorder({"upload_file_id": "image-1", "byte_size": 8, "mime_type": "image/png"})
monkeypatch.setattr(resources, "upload_service_query_image", upload)
with Flask(__name__).test_request_context("/", method="POST", data={"file": (io.BytesIO(b"fakepng"), "image.png")}):
response, status = resources.KnowledgeFSServiceQueryImagesApi().post("control-1")
assert status == 201
assert response == {"upload_file_id": "image-1", "byte_size": 8, "mime_type": "image/png"}
assert upload.call_args.kwargs["profile"].tenant_id == "tenant-1"
assert upload.call_args.kwargs["file"].filename == "image.png"
assert upload.last_call.kwargs["profile"].tenant_id == "tenant-1"
assert upload.last_call.kwargs["file"].filename == "image.png"
def test_image_admission_checks_ownership_before_issuing_a_capability(runtime, monkeypatch: pytest.MonkeyPatch) -> None:
from core.knowledge_fs.errors import KnowledgeFSProductRequestRejectedError
runtime.broker = MagicMock()
validator = MagicMock(side_effect=KnowledgeFSProductRequestRejectedError(status_code=422))
runtime.broker = FakeComponent(issue_service=Recorder())
validator = Recorder(raises=KnowledgeFSProductRequestRejectedError(status_code=422))
monkeypatch.setattr(resources, "validate_service_query_image_references", validator)
with Flask(__name__).test_request_context(
"/",
@@ -339,8 +349,8 @@ def test_image_admission_checks_ownership_before_issuing_a_capability(runtime, m
with pytest.raises(Exception) as error:
resources.KnowledgeFSServiceQueryAdmissionApi().post("control-1")
assert error.value.code == 422
runtime.broker.issue_service.assert_not_called()
validator.assert_called_once()
assert runtime.broker.issue_service.calls == []
assert len(validator.calls) == 1
def test_atomic_crawl_edit_rejects_inline_document_content() -> None:
@@ -411,15 +421,15 @@ def test_recovery_reads_keep_the_authorized_space_and_child_resource_binding(
path_parameters,
) -> None:
profile = object()
authorize = MagicMock(return_value=profile)
authorize = Recorder(profile)
monkeypatch.setattr(resources, "_profile", authorize)
monkeypatch.setattr(resources, "_dump_response", lambda _schema, raw: raw)
upstream = {"id": resource_id}
runtime.facade.execute_service.return_value = upstream
with Flask(__name__).test_request_context("/", method="GET"):
assert controller().get(*arguments) is upstream
authorize.assert_called_once_with(runtime, operation_id=operation, control_space_id="control-1")
call = runtime.facade.execute_service.call_args.kwargs
assert authorize.calls == [invocation(runtime, operation_id=operation, control_space_id="control-1")]
call = runtime.facade.execute_service.last_call.kwargs
assert call["profile"] is profile
assert call["operation_id"] == operation
assert call["resource_id"] == resource_id
@@ -432,7 +442,7 @@ def test_source_workflow_history_keeps_source_filter_and_bounded_pagination(
monkeypatch.setattr(resources, "_dump_response", lambda _schema, raw: raw)
with Flask(__name__).test_request_context("/?source_id=source-1&cursor=next-page&limit=25"):
resources.KnowledgeFSServiceSourceWorkflowsApi().get("control-1")
call = runtime.facade.execute_service.call_args.kwargs
call = runtime.facade.execute_service.last_call.kwargs
assert call["operation_id"] == "listSourceWorkflows"
assert dict(call["query"]) == {"sourceId": "source-1", "cursor": "next-page", "limit": "25"}
@@ -448,7 +458,7 @@ def test_preview_selection_is_a_durable_idempotent_import_admission(runtime) ->
result, status = resources.KnowledgeFSServiceCrawlPreviewSelectionApi().post("control-1", "run-1")
assert status == 202
assert result["id"] == "run-1"
call = runtime.facade.execute_service.call_args.kwargs
call = runtime.facade.execute_service.last_call.kwargs
assert call["operation_id"] == "selectCrawlPreviewPages"
assert call["resource_id"] == "run-1"
assert call["headers"] == (("Idempotency-Key", "selection-request-1"),)
@@ -600,7 +610,7 @@ def test_durable_source_contracts_bind_the_original_profile_and_resource(
requires_key,
) -> None:
profile = object()
authorize = MagicMock(return_value=profile)
authorize = Recorder(profile)
monkeypatch.setattr(resources, "_profile", authorize)
monkeypatch.setattr(resources, "_dump_response", lambda _schema, raw: raw)
with Flask(__name__).test_request_context(
@@ -610,8 +620,8 @@ def test_durable_source_contracts_bind_the_original_profile_and_resource(
headers={"Idempotency-Key": "source-command-1"},
):
method(controller(), *arguments)
authorize.assert_called_once_with(runtime, operation_id=operation, control_space_id="control-1")
call = runtime.facade.execute_service.call_args.kwargs
assert authorize.calls == [invocation(runtime, operation_id=operation, control_space_id="control-1")]
call = runtime.facade.execute_service.last_call.kwargs
assert call["profile"] is profile
assert call["resource_id"] == resource_id
assert call["path_parameters"] == path_parameters