mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 09:56:06 +00:00
214 lines
7.5 KiB
Python
214 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
import types
|
|
from importlib import import_module
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
import quart
|
|
|
|
core_app_module = types.ModuleType('langbot.pkg.core.app')
|
|
core_app_module.Application = object
|
|
sys.modules.setdefault('langbot.pkg.core.app', core_app_module)
|
|
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
async def _create_test_client(pipeline_service: SimpleNamespace, **extra_ap):
|
|
app = quart.Quart(__name__)
|
|
user_service = SimpleNamespace(
|
|
verify_jwt_token=AsyncMock(return_value='test@example.com'),
|
|
get_user_by_email=AsyncMock(return_value=SimpleNamespace(user='test@example.com')),
|
|
)
|
|
ap = SimpleNamespace(pipeline_service=pipeline_service, user_service=user_service, **extra_ap)
|
|
router_class = import_module('langbot.pkg.api.http.controller.groups.pipelines.pipelines').PipelinesRouterGroup
|
|
group = router_class(ap, app)
|
|
await group.initialize()
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('method', 'path', 'service_method'),
|
|
[
|
|
('post', '/api/v1/pipelines', 'create_pipeline'),
|
|
('put', '/api/v1/pipelines/pipeline-1', 'update_pipeline'),
|
|
],
|
|
)
|
|
async def test_pipeline_writes_return_bad_request_for_invalid_runner_security_field(
|
|
method,
|
|
path,
|
|
service_method,
|
|
):
|
|
message = "Pipeline runner_config['runner-1'] field 'enable-all-tools' must be a boolean"
|
|
pipeline_service = SimpleNamespace(
|
|
create_pipeline=AsyncMock(),
|
|
update_pipeline=AsyncMock(),
|
|
update_pipeline_extensions=AsyncMock(),
|
|
)
|
|
getattr(pipeline_service, service_method).side_effect = ValueError(message)
|
|
client = await _create_test_client(pipeline_service)
|
|
|
|
response = await getattr(client, method)(
|
|
path,
|
|
json={'config': {'ai': {'runner': {'id': 'runner-1'}}}},
|
|
headers={'Authorization': 'Bearer test-token'},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert await response.get_json() == {'code': -1, 'msg': message}
|
|
|
|
|
|
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
|
async def test_pipeline_extensions_reject_non_boolean_resource_read(invalid_value):
|
|
pipeline_service = SimpleNamespace(
|
|
create_pipeline=AsyncMock(),
|
|
update_pipeline=AsyncMock(),
|
|
update_pipeline_extensions=AsyncMock(),
|
|
)
|
|
client = await _create_test_client(pipeline_service)
|
|
|
|
response = await client.put(
|
|
'/api/v1/pipelines/pipeline-1/extensions',
|
|
json={'mcp_resource_agent_read_enabled': invalid_value},
|
|
headers={'Authorization': 'Bearer test-token'},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert await response.get_json() == {
|
|
'code': -1,
|
|
'msg': "Pipeline extension field 'mcp_resource_agent_read_enabled' must be a boolean",
|
|
}
|
|
pipeline_service.update_pipeline_extensions.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'field',
|
|
[
|
|
'enable_all_plugins',
|
|
'enable_all_mcp_servers',
|
|
'enable_all_skills',
|
|
],
|
|
)
|
|
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
|
async def test_pipeline_extensions_reject_non_boolean_enable_all_flags(field, invalid_value):
|
|
pipeline_service = SimpleNamespace(
|
|
create_pipeline=AsyncMock(),
|
|
update_pipeline=AsyncMock(),
|
|
update_pipeline_extensions=AsyncMock(),
|
|
)
|
|
client = await _create_test_client(pipeline_service)
|
|
|
|
response = await client.put(
|
|
'/api/v1/pipelines/pipeline-1/extensions',
|
|
json={field: invalid_value},
|
|
headers={'Authorization': 'Bearer test-token'},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert await response.get_json() == {
|
|
'code': -1,
|
|
'msg': f"Pipeline extension field '{field}' must be a boolean",
|
|
}
|
|
pipeline_service.update_pipeline_extensions.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('field', 'invalid_value'),
|
|
[
|
|
('bound_plugins', 'author/plugin'),
|
|
('bound_mcp_servers', 'server-1'),
|
|
('bound_skills', 'skill-1'),
|
|
('bound_mcp_resources', {'uri': 'file:///README.md'}),
|
|
],
|
|
)
|
|
async def test_pipeline_extensions_return_bad_request_for_malformed_binding_lists(
|
|
field,
|
|
invalid_value,
|
|
):
|
|
message = f"Pipeline extension field '{field}' must be a list"
|
|
pipeline_service = SimpleNamespace(
|
|
create_pipeline=AsyncMock(),
|
|
update_pipeline=AsyncMock(),
|
|
update_pipeline_extensions=AsyncMock(side_effect=ValueError(message)),
|
|
)
|
|
client = await _create_test_client(pipeline_service)
|
|
|
|
response = await client.put(
|
|
'/api/v1/pipelines/pipeline-1/extensions',
|
|
json={field: invalid_value},
|
|
headers={'Authorization': 'Bearer test-token'},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert await response.get_json() == {'code': -1, 'msg': message}
|
|
|
|
|
|
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
|
async def test_pipeline_extensions_get_normalizes_malformed_enable_all_flags(invalid_value):
|
|
pipeline_service = SimpleNamespace(
|
|
get_pipeline=AsyncMock(
|
|
return_value={
|
|
'extensions_preferences': {
|
|
'enable_all_plugins': invalid_value,
|
|
'enable_all_mcp_servers': invalid_value,
|
|
'enable_all_skills': invalid_value,
|
|
'mcp_resource_agent_read_enabled': invalid_value,
|
|
}
|
|
}
|
|
),
|
|
)
|
|
client = await _create_test_client(
|
|
pipeline_service,
|
|
plugin_connector=SimpleNamespace(list_plugins=AsyncMock(return_value=[])),
|
|
mcp_service=SimpleNamespace(get_mcp_servers=AsyncMock(return_value=[])),
|
|
skill_service=SimpleNamespace(list_skills=AsyncMock(return_value=[])),
|
|
logger=SimpleNamespace(warning=AsyncMock()),
|
|
)
|
|
|
|
response = await client.get(
|
|
'/api/v1/pipelines/pipeline-1/extensions',
|
|
headers={'Authorization': 'Bearer test-token'},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = await response.get_json()
|
|
assert payload['data']['enable_all_plugins'] is False
|
|
assert payload['data']['enable_all_mcp_servers'] is False
|
|
assert payload['data']['enable_all_skills'] is False
|
|
assert payload['data']['mcp_resource_agent_read_enabled'] is False
|
|
|
|
|
|
@pytest.mark.parametrize('invalid_preferences', [None, [], 'all', 0, False])
|
|
async def test_pipeline_extensions_get_malformed_root_is_fail_closed(invalid_preferences):
|
|
pipeline_service = SimpleNamespace(
|
|
get_pipeline=AsyncMock(
|
|
return_value={'extensions_preferences': invalid_preferences}
|
|
),
|
|
)
|
|
client = await _create_test_client(
|
|
pipeline_service,
|
|
plugin_connector=SimpleNamespace(list_plugins=AsyncMock(return_value=[])),
|
|
mcp_service=SimpleNamespace(get_mcp_servers=AsyncMock(return_value=[])),
|
|
skill_service=SimpleNamespace(list_skills=AsyncMock(return_value=[])),
|
|
logger=SimpleNamespace(warning=AsyncMock()),
|
|
)
|
|
|
|
response = await client.get(
|
|
'/api/v1/pipelines/pipeline-1/extensions',
|
|
headers={'Authorization': 'Bearer test-token'},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = await response.get_json()
|
|
assert payload['data']['enable_all_plugins'] is False
|
|
assert payload['data']['enable_all_mcp_servers'] is False
|
|
assert payload['data']['enable_all_skills'] is False
|
|
assert payload['data']['mcp_resource_agent_read_enabled'] is False
|
|
assert payload['data']['bound_plugins'] == []
|
|
assert payload['data']['bound_mcp_servers'] == []
|
|
assert payload['data']['bound_skills'] == []
|
|
assert payload['data']['bound_mcp_resources'] == []
|