mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-09-22 05:02:58 +00:00
* feat: 初始化插件系统 * refactor: 收口插件系统运行时重构 * perf: 优化插件系统类型提示 * fix&perf: 修复和优化插件系统 * fix: 修复gitignore规则误忽略插件文件的问题 * fix: 修复运行时插件根路径算错的问题 * fix: 加强插件发现和路由注册的防护措施 * revert: 回滚定时任务白名单 * fix: 移除未使用的应用路由注册探测 * revert: 恢复部分代码 * perf: 优化插件系统 * docs: 新增插件开发文档 * perf: 优化插件管理模块 * perf: 提升插件系统核心能力 * refactor: 重构生命周期 step runner * fix: 修复lint错误 * test: 清理测试用例 * test: 调整测试目录名称 * fix: 修复前后端目录硬编码的问题 * fix: 修复插件系统安全性缺口 * refactor: 重新设计插件生命周期 Migration 事务与回滚 * perf: 优化插件系统边界问题 * refactor: 重构当前插件系统的依赖体系设计 * perf: 优化代码 * perf: 优化代码 * fix: 修复代码合并问题 * fix: 修复bug * perf: 优化代码 * perf&fix: 优化代码和修复bug * docs: 优化文档格式 * feat: 适配Vue2版本 * docs: 更新README文档 * fix: 修复ruff lint错误 * chore: 更新后端依赖文件
603 lines
20 KiB
Python
603 lines
20 KiB
Python
import json
|
|
from collections.abc import Generator
|
|
from datetime import datetime
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from common.annotation import log_annotation
|
|
from common.context import RequestContext
|
|
from common.vo import PageModel
|
|
from module_admin.entity.vo.user_vo import CurrentUserModel, UserInfoModel
|
|
from module_plugin.controller import plugin_controller
|
|
from plugins.core.management.entity.vo.schemas import (
|
|
PluginBatchActionModel,
|
|
PluginOperationLogDetailModel,
|
|
PluginOperationLogExportQueryModel,
|
|
PluginOperationLogPageQueryModel,
|
|
PluginOperationLogRetentionModel,
|
|
PluginOperationLogRetentionResultModel,
|
|
)
|
|
|
|
HTTP_OK = 200
|
|
EXPECTED_RETENTION_MATCHED_COUNT = 2
|
|
PLUGIN_OPERATION_FAILURE_EXIT_CODE = 10
|
|
|
|
|
|
def build_request(path: str = '/system/plugin/batch', method: str = 'POST') -> object:
|
|
"""构造测试用请求对象。"""
|
|
|
|
async def empty_body() -> bytes:
|
|
"""返回测试用空请求体。"""
|
|
return b''
|
|
|
|
return SimpleNamespace(
|
|
url=SimpleNamespace(path=path),
|
|
method=method,
|
|
headers={'User-Agent': 'pytest'},
|
|
client=SimpleNamespace(host='127.0.0.1'),
|
|
query_params={},
|
|
path_params={},
|
|
body=empty_body,
|
|
)
|
|
|
|
|
|
def load_response_body(response: object) -> dict:
|
|
"""解析测试响应体。"""
|
|
return json.loads(response.body.decode())
|
|
|
|
|
|
def test_plugin_operation_response_hides_exit_code_from_web_payload() -> None:
|
|
"""校验 Web 插件操作响应不会暴露 CLI 退出码。"""
|
|
payload = {
|
|
'ok': True,
|
|
'message': '插件操作完成',
|
|
'pluginId': 'demo',
|
|
'exit_code': PLUGIN_OPERATION_FAILURE_EXIT_CODE,
|
|
}
|
|
|
|
response = plugin_controller._plugin_operation_response(payload, '插件操作完成')
|
|
body = load_response_body(response)
|
|
|
|
assert body['success'] is True
|
|
assert body['data']['pluginId'] == 'demo'
|
|
assert 'exit_code' not in body['data']
|
|
assert payload['exit_code'] == PLUGIN_OPERATION_FAILURE_EXIT_CODE
|
|
|
|
|
|
class FakeAsyncSession:
|
|
"""
|
|
测试用异步数据库会话。
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""初始化测试会话。"""
|
|
self.committed = False
|
|
|
|
async def commit(self) -> None:
|
|
"""记录事务提交。"""
|
|
self.committed = True
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def fake_request_context(monkeypatch: pytest.MonkeyPatch) -> Generator[CurrentUserModel, None, None]:
|
|
"""构造日志装饰器所需的最小请求上下文。"""
|
|
user = CurrentUserModel(
|
|
permissions=['*:*:*'],
|
|
roles=[],
|
|
user=UserInfoModel(userName='admin', dept=None, role=[]),
|
|
)
|
|
user_token = RequestContext.set_current_user(user)
|
|
pattern_token = RequestContext.set_current_exclude_patterns([])
|
|
|
|
async def fake_enqueue_operation_log(*args: object, **kwargs: object) -> None:
|
|
"""记录测试中的操作日志入队请求。"""
|
|
|
|
monkeypatch.setattr(log_annotation.LogQueueService, 'enqueue_operation_log', fake_enqueue_operation_log)
|
|
try:
|
|
yield user
|
|
finally:
|
|
RequestContext.reset_current_user(user_token)
|
|
RequestContext.reset_current_exclude_patterns(pattern_token)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_system_plugins_accepts_json_body(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""校验批量操作接口使用 JSON 请求体。"""
|
|
recorded: dict[str, object] = {}
|
|
|
|
class FakePluginRuntimeService:
|
|
"""
|
|
测试用插件运行时服务。
|
|
"""
|
|
|
|
async def batch_plugins(
|
|
self,
|
|
operation: str,
|
|
plugin_ids: list[str] | None = None,
|
|
*,
|
|
dry_run: bool = True,
|
|
continue_on_error: bool = False,
|
|
) -> dict[str, object]:
|
|
"""记录批量操作参数。"""
|
|
recorded.update(
|
|
{
|
|
'operation': operation,
|
|
'plugin_ids': plugin_ids,
|
|
'dry_run': dry_run,
|
|
'continue_on_error': continue_on_error,
|
|
}
|
|
)
|
|
return {'ok': True, 'message': '批量操作完成'}
|
|
|
|
monkeypatch.setattr(plugin_controller, 'get_plugin_runtime_service', FakePluginRuntimeService)
|
|
|
|
response = await plugin_controller.batch_system_plugins(
|
|
request=build_request('/system/plugin/batch'),
|
|
batch_action=PluginBatchActionModel(
|
|
operation='install',
|
|
dryRun=True,
|
|
continueOnError=True,
|
|
pluginIds=['demo', 'ai'],
|
|
),
|
|
)
|
|
|
|
assert response.status_code == HTTP_OK
|
|
assert recorded == {
|
|
'operation': 'install',
|
|
'plugin_ids': ['demo', 'ai'],
|
|
'dry_run': True,
|
|
'continue_on_error': True,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_system_plugin_accepts_dry_run_query_param(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
fake_request_context: CurrentUserModel,
|
|
) -> None:
|
|
"""校验单插件安装接口使用展开后的 dryRun 查询参数。"""
|
|
recorded: dict[str, object] = {}
|
|
|
|
class FakePluginRuntimeService:
|
|
"""
|
|
测试用插件运行时服务。
|
|
"""
|
|
|
|
async def install_plugin(
|
|
self,
|
|
plugin_id: str,
|
|
*,
|
|
dry_run: bool = False,
|
|
operated_by: str | None = None,
|
|
) -> dict[str, object]:
|
|
"""记录安装参数。"""
|
|
recorded.update({'plugin_id': plugin_id, 'dry_run': dry_run})
|
|
return {'ok': True, 'message': '安装完成'}
|
|
|
|
monkeypatch.setattr(plugin_controller, 'get_plugin_runtime_service', FakePluginRuntimeService)
|
|
|
|
response = await plugin_controller.install_system_plugin(
|
|
request=build_request('/system/plugin/demo/install'),
|
|
plugin_id='demo',
|
|
current_user=fake_request_context,
|
|
dry_run=True,
|
|
)
|
|
|
|
assert response.status_code == HTTP_OK
|
|
assert recorded == {'plugin_id': 'demo', 'dry_run': True}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_system_plugin_returns_failure_when_runtime_payload_is_not_ok(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
fake_request_context: CurrentUserModel,
|
|
) -> None:
|
|
"""校验安装运行时返回失败时接口返回失败响应。"""
|
|
|
|
class FakePluginRuntimeService:
|
|
"""
|
|
测试用插件运行时服务。
|
|
"""
|
|
|
|
async def install_plugin(
|
|
self,
|
|
plugin_id: str,
|
|
*,
|
|
dry_run: bool = False,
|
|
operated_by: str | None = None,
|
|
) -> dict[str, object]:
|
|
"""返回失败安装结果。"""
|
|
return {'ok': False, 'message': '插件安装失败', 'pluginId': plugin_id}
|
|
|
|
monkeypatch.setattr(plugin_controller, 'get_plugin_runtime_service', FakePluginRuntimeService)
|
|
|
|
response = await plugin_controller.install_system_plugin(
|
|
request=build_request('/system/plugin/demo/install'),
|
|
plugin_id='demo',
|
|
current_user=fake_request_context,
|
|
dry_run=False,
|
|
)
|
|
body = load_response_body(response)
|
|
|
|
assert response.status_code == HTTP_OK
|
|
assert body['success'] is False
|
|
assert body['msg'] == '插件安装失败'
|
|
assert body['data']['ok'] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_install_system_plugin_dependencies_forces_dry_run(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""校验 Web 依赖安装接口始终只生成 dry-run 安装计划。"""
|
|
recorded: dict[str, object] = {}
|
|
|
|
class FakePluginRuntimeService:
|
|
"""
|
|
测试用插件运行时服务。
|
|
"""
|
|
|
|
def install_plugin_dependencies(
|
|
self,
|
|
plugin_id: str,
|
|
*,
|
|
dry_run: bool = True,
|
|
policy_config: object | None = None,
|
|
) -> dict[str, object]:
|
|
"""记录依赖安装参数。"""
|
|
recorded.update({'plugin_id': plugin_id, 'dry_run': dry_run, 'policy_mode': policy_config.mode})
|
|
return {'ok': True, 'message': '插件依赖安装演练完成', 'pluginId': plugin_id}
|
|
|
|
monkeypatch.setattr(plugin_controller, 'get_plugin_runtime_service', FakePluginRuntimeService)
|
|
|
|
response = await plugin_controller.install_system_plugin_dependencies(
|
|
request=build_request('/system/plugin/demo/dependencies/install'),
|
|
plugin_id='demo',
|
|
dry_run=False,
|
|
)
|
|
body = load_response_body(response)
|
|
|
|
assert response.status_code == HTTP_OK
|
|
assert body['success'] is True
|
|
assert recorded == {'plugin_id': 'demo', 'dry_run': True, 'policy_mode': 'plan_only'}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_enable_system_plugin_returns_runtime_payload_when_not_ok(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""校验启用插件失败时接口透传运行时失败负载。"""
|
|
|
|
class FakePluginRuntimeService:
|
|
"""
|
|
测试用插件运行时服务。
|
|
"""
|
|
|
|
async def set_plugin_enabled(self, plugin_id: str, *, enabled: bool, dry_run: bool = False) -> dict:
|
|
"""返回失败启用结果。"""
|
|
return {'ok': False, 'message': '启用失败', 'pluginId': plugin_id, 'enabled': enabled}
|
|
|
|
monkeypatch.setattr(plugin_controller, 'get_plugin_runtime_service', FakePluginRuntimeService)
|
|
|
|
response = await plugin_controller.enable_system_plugin(
|
|
request=build_request('/system/plugin/demo/enable', method='PUT'),
|
|
plugin_id='demo',
|
|
query_db=FakeAsyncSession(),
|
|
)
|
|
body = load_response_body(response)
|
|
|
|
assert response.status_code == HTTP_OK
|
|
assert body['success'] is False
|
|
assert body['msg'] == '启用失败'
|
|
assert body['data']['ok'] is False
|
|
assert body['data']['pluginId'] == 'demo'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disable_system_plugin_delegates_runtime_service(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""校验停用插件接口走运行时服务,确保内部事务会被提交。"""
|
|
recorded: dict[str, object] = {}
|
|
|
|
class FakePluginRuntimeService:
|
|
"""
|
|
测试用插件运行时服务。
|
|
"""
|
|
|
|
async def set_plugin_enabled(self, plugin_id: str, *, enabled: bool, dry_run: bool = False) -> dict:
|
|
"""记录插件启停参数。"""
|
|
recorded.update({'plugin_id': plugin_id, 'enabled': enabled, 'dry_run': dry_run})
|
|
return {'ok': True, 'message': '停用成功', 'pluginId': plugin_id, 'enabled': enabled}
|
|
|
|
monkeypatch.setattr(plugin_controller, 'get_plugin_runtime_service', FakePluginRuntimeService)
|
|
|
|
response = await plugin_controller.disable_system_plugin(
|
|
request=build_request('/system/plugin/demo/disable', method='PUT'),
|
|
plugin_id='demo',
|
|
)
|
|
body = load_response_body(response)
|
|
|
|
assert response.status_code == HTTP_OK
|
|
assert recorded == {'plugin_id': 'demo', 'enabled': False, 'dry_run': False}
|
|
assert body['success'] is True
|
|
assert body['msg'] == '停用成功'
|
|
assert body['data']['pluginId'] == 'demo'
|
|
assert body['data']['enabled'] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_system_plugin_operation_log_list_returns_page_payload(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""校验审计列表接口会返回分页负载并透传查询条件。"""
|
|
recorded: dict[str, object] = {}
|
|
operation_log = PluginOperationLogDetailModel(
|
|
operationId=1,
|
|
operation='install',
|
|
pluginIds=['demo'],
|
|
dryRun=False,
|
|
continueOnError=False,
|
|
status='success',
|
|
summary={'total': 1},
|
|
result={'ok': True},
|
|
remark='安装完成',
|
|
)
|
|
page_result = PageModel[PluginOperationLogDetailModel](
|
|
rows=[operation_log],
|
|
pageNum=1,
|
|
pageSize=10,
|
|
total=1,
|
|
hasNext=False,
|
|
)
|
|
|
|
class FakePluginService:
|
|
"""
|
|
测试用插件管理服务。
|
|
"""
|
|
|
|
@classmethod
|
|
async def get_plugin_operation_log_page_list_services(
|
|
cls,
|
|
query_db: object,
|
|
query_object: PluginOperationLogPageQueryModel,
|
|
is_page: bool = True,
|
|
) -> PageModel[PluginOperationLogDetailModel]:
|
|
"""记录审计列表查询参数。"""
|
|
recorded.update(
|
|
{
|
|
'query_db': query_db,
|
|
'plugin_id': query_object.plugin_id,
|
|
'operation': query_object.operation,
|
|
'status': query_object.status,
|
|
'is_page': is_page,
|
|
}
|
|
)
|
|
return page_result
|
|
|
|
monkeypatch.setattr(plugin_controller, 'PluginService', FakePluginService)
|
|
query_db = FakeAsyncSession()
|
|
|
|
response = await plugin_controller.get_system_plugin_operation_log_list(
|
|
request=build_request('/system/plugin/operation-log/list', method='GET'),
|
|
operation_log_page_query=PluginOperationLogPageQueryModel(
|
|
pluginId='demo',
|
|
operation='install',
|
|
status='success',
|
|
pageNum=1,
|
|
pageSize=10,
|
|
),
|
|
query_db=query_db,
|
|
)
|
|
|
|
body = load_response_body(response)
|
|
assert response.status_code == HTTP_OK
|
|
assert body['success'] is True
|
|
assert body['total'] == 1
|
|
assert body['rows'][0]['operationId'] == 1
|
|
assert body['rows'][0]['pluginIds'] == ['demo']
|
|
assert recorded == {
|
|
'query_db': query_db,
|
|
'plugin_id': 'demo',
|
|
'operation': 'install',
|
|
'status': 'success',
|
|
'is_page': True,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_detail_system_plugin_operation_log_returns_payload(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""校验审计详情接口会返回插件操作审计详情。"""
|
|
recorded: dict[str, object] = {}
|
|
operation_log = PluginOperationLogDetailModel(
|
|
operationId=1,
|
|
operation='install',
|
|
pluginIds=['demo'],
|
|
dryRun=False,
|
|
continueOnError=False,
|
|
status='success',
|
|
summary={'total': 1},
|
|
result={'ok': True},
|
|
remark='安装完成',
|
|
)
|
|
|
|
class FakePluginService:
|
|
"""
|
|
测试用插件管理服务。
|
|
"""
|
|
|
|
@classmethod
|
|
async def plugin_operation_log_detail_services(
|
|
cls,
|
|
query_db: object,
|
|
operation_id: int,
|
|
) -> PluginOperationLogDetailModel:
|
|
"""记录审计详情查询参数。"""
|
|
recorded.update({'query_db': query_db, 'operation_id': operation_id})
|
|
return operation_log
|
|
|
|
monkeypatch.setattr(plugin_controller, 'PluginService', FakePluginService)
|
|
query_db = FakeAsyncSession()
|
|
|
|
response = await plugin_controller.query_detail_system_plugin_operation_log(
|
|
request=build_request('/system/plugin/operation-log/1', method='GET'),
|
|
operation_id=1,
|
|
query_db=query_db,
|
|
)
|
|
|
|
body = load_response_body(response)
|
|
assert response.status_code == HTTP_OK
|
|
assert body['success'] is True
|
|
assert body['data']['operationId'] == 1
|
|
assert body['data']['pluginIds'] == ['demo']
|
|
assert recorded == {'query_db': query_db, 'operation_id': 1}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_detail_system_plugin_operation_log_returns_failure_when_missing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""校验审计详情不存在时返回失败响应。"""
|
|
|
|
class FakePluginService:
|
|
"""
|
|
测试用插件管理服务。
|
|
"""
|
|
|
|
@classmethod
|
|
async def plugin_operation_log_detail_services(cls, query_db: object, operation_id: int) -> None:
|
|
"""返回空审计详情。"""
|
|
return
|
|
|
|
monkeypatch.setattr(plugin_controller, 'PluginService', FakePluginService)
|
|
|
|
response = await plugin_controller.query_detail_system_plugin_operation_log(
|
|
request=build_request('/system/plugin/operation-log/404', method='GET'),
|
|
operation_id=404,
|
|
query_db=FakeAsyncSession(),
|
|
)
|
|
|
|
body = load_response_body(response)
|
|
assert response.status_code == HTTP_OK
|
|
assert body['success'] is False
|
|
assert body['msg'] == '插件批量操作审计日志不存在'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retain_system_plugin_operation_log_commits_session(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""校验审计保留策略接口会调用管理服务并提交事务。"""
|
|
recorded: dict[str, object] = {}
|
|
retention_result = PluginOperationLogRetentionResultModel(
|
|
retentionDays=0,
|
|
cutoffTime=datetime(2026, 5, 22, 12, 0, 0),
|
|
matchedCount=2,
|
|
deletedCount=0,
|
|
dryRun=True,
|
|
)
|
|
|
|
class FakePluginService:
|
|
"""
|
|
测试用插件管理服务。
|
|
"""
|
|
|
|
@classmethod
|
|
async def retain_plugin_operation_log_services(
|
|
cls,
|
|
query_db: object,
|
|
retention_model: PluginOperationLogRetentionModel,
|
|
) -> PluginOperationLogRetentionResultModel:
|
|
"""记录审计保留策略调用参数。"""
|
|
recorded.update({'query_db': query_db, 'retention_days': retention_model.retention_days})
|
|
return retention_result
|
|
|
|
monkeypatch.setattr(plugin_controller, 'PluginService', FakePluginService)
|
|
query_db = FakeAsyncSession()
|
|
|
|
response = await plugin_controller.retain_system_plugin_operation_log(
|
|
request=build_request('/system/plugin/operation-log/retention', method='DELETE'),
|
|
retention_query=PluginOperationLogRetentionModel(retentionDays=0, dryRun=True),
|
|
query_db=query_db,
|
|
)
|
|
|
|
body = load_response_body(response)
|
|
assert response.status_code == HTTP_OK
|
|
assert body['success'] is True
|
|
assert body['data']['retentionDays'] == 0
|
|
assert body['data']['matchedCount'] == EXPECTED_RETENTION_MATCHED_COUNT
|
|
assert query_db.committed is True
|
|
assert recorded == {'query_db': query_db, 'retention_days': 0}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_system_plugin_operation_log_uses_operation_dict(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""校验审计导出接口会使用操作类型字典映射。"""
|
|
recorded: dict[str, object] = {}
|
|
operation_log = PluginOperationLogDetailModel(
|
|
operationId=1,
|
|
operation='config_set',
|
|
pluginIds=['demo'],
|
|
dryRun=False,
|
|
continueOnError=False,
|
|
status='success',
|
|
summary={},
|
|
result={},
|
|
remark='配置保存',
|
|
)
|
|
|
|
class FakePluginService:
|
|
"""
|
|
测试用插件管理服务。
|
|
"""
|
|
|
|
@classmethod
|
|
async def get_plugin_operation_log_export_list_services(
|
|
cls,
|
|
query_db: object,
|
|
query_object: PluginOperationLogExportQueryModel,
|
|
) -> list[PluginOperationLogDetailModel]:
|
|
"""记录审计导出查询参数。"""
|
|
recorded.update({'query_db': query_db, 'export_limit': query_object.export_limit})
|
|
return [operation_log]
|
|
|
|
@classmethod
|
|
def export_plugin_operation_log_list_services(
|
|
cls,
|
|
operation_log_list: list[PluginOperationLogDetailModel],
|
|
operation_dict: dict[str, str],
|
|
) -> bytes:
|
|
"""记录审计导出字典映射。"""
|
|
recorded.update(
|
|
{
|
|
'operation_count': len(operation_log_list),
|
|
'operation_dict': operation_dict,
|
|
}
|
|
)
|
|
return b'plugin audit export'
|
|
|
|
class FakePluginOperationService:
|
|
"""
|
|
测试用插件操作服务。
|
|
"""
|
|
|
|
@classmethod
|
|
async def get_plugin_operation_dict_services(cls, query_db: object) -> dict[str, str]:
|
|
"""返回测试用操作类型字典。"""
|
|
recorded['dict_query_db'] = query_db
|
|
return {'config_set': '配置保存'}
|
|
|
|
monkeypatch.setattr(plugin_controller, 'PluginService', FakePluginService)
|
|
monkeypatch.setattr(plugin_controller, 'PluginOperationService', FakePluginOperationService)
|
|
query_db = FakeAsyncSession()
|
|
|
|
response = await plugin_controller.export_system_plugin_operation_log_list(
|
|
request=build_request('/system/plugin/operation-log/export'),
|
|
operation_log_export_query=PluginOperationLogExportQueryModel(exportLimit=10),
|
|
query_db=query_db,
|
|
)
|
|
chunks = [chunk async for chunk in response.body_iterator]
|
|
|
|
assert response.status_code == HTTP_OK
|
|
assert b''.join(chunks) == b'plugin audit export'
|
|
assert recorded == {
|
|
'query_db': query_db,
|
|
'export_limit': 10,
|
|
'dict_query_db': query_db,
|
|
'operation_count': 1,
|
|
'operation_dict': {'config_set': '配置保存'},
|
|
}
|