mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-11 11:59:06 +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: 更新后端依赖文件
162 lines
6.2 KiB
Python
162 lines
6.2 KiB
Python
import asyncio
|
|
import json
|
|
from datetime import datetime
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from sqlalchemy import Text
|
|
|
|
from config.env import LogConfig
|
|
from module_admin.dao.file_access_dao import FileAccessLogDao
|
|
from module_admin.entity.do.file_do import SysFileAccessLog
|
|
from module_admin.entity.vo.file_vo import FileAccessLogModel
|
|
from module_admin.service.file_access_service import FileAuditService
|
|
from module_admin.service.log_service import LogAggregatorService, LogQueueService
|
|
|
|
FILE_SIZE = 12
|
|
BATCH_FILE_COUNT = 2
|
|
LONG_OPERATION_DETAIL_LENGTH = 3000
|
|
|
|
|
|
class AsyncSessionContext:
|
|
"""测试用异步数据库会话上下文。"""
|
|
|
|
def __init__(self, session: SimpleNamespace) -> None:
|
|
self.session = session
|
|
|
|
async def __aenter__(self) -> SimpleNamespace:
|
|
return self.session
|
|
|
|
async def __aexit__(self, exc_type: type | None, exc_value: BaseException | None, traceback: object) -> None:
|
|
return None
|
|
|
|
|
|
def make_file_access_log() -> FileAccessLogModel:
|
|
return FileAccessLogModel(
|
|
fileId='file-id',
|
|
action='download',
|
|
actorUserId=10,
|
|
actorName='user10',
|
|
result='completed',
|
|
requestId='request-id',
|
|
traceId='trace-id',
|
|
ipAddress='127.0.0.1',
|
|
userAgent='pytest',
|
|
bytesSent=FILE_SIZE,
|
|
operationDetail='{"newStatus":"active"}',
|
|
accessTime=datetime(2026, 7, 19, 12, 0, 0),
|
|
)
|
|
|
|
|
|
def test_file_access_log_operation_detail_uses_unbounded_text_type() -> None:
|
|
operation_detail_column = SysFileAccessLog.__table__.c.operation_detail
|
|
|
|
assert isinstance(operation_detail_column.type, Text)
|
|
assert operation_detail_column.type.length is None
|
|
|
|
|
|
def test_file_access_log_is_enqueued_to_redis_stream() -> None:
|
|
redis = SimpleNamespace(xadd=AsyncMock())
|
|
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(redis=redis)))
|
|
|
|
with (
|
|
patch('module_admin.service.log_service.TraceCtx.get_request_id', return_value='request-id'),
|
|
patch('module_admin.service.log_service.TraceCtx.get_trace_id', return_value='trace-id'),
|
|
patch('module_admin.service.log_service.TraceCtx.get_span_id', return_value='span-id'),
|
|
):
|
|
asyncio.run(LogQueueService.enqueue_file_access_log(request, make_file_access_log(), 'file:download:completed'))
|
|
|
|
redis.xadd.assert_awaited_once()
|
|
stream_name, event = redis.xadd.await_args.args
|
|
assert stream_name == LogConfig.log_stream_key
|
|
assert event['event_type'] == 'file_access'
|
|
payload = json.loads(event['payload'])
|
|
assert payload['fileId'] == 'file-id'
|
|
assert payload['result'] == 'completed'
|
|
assert payload['bytesSent'] == FILE_SIZE
|
|
assert payload['operationDetail'] == '{"newStatus":"active"}'
|
|
|
|
|
|
def test_file_access_log_event_is_persisted_and_acknowledged() -> None:
|
|
session = SimpleNamespace(commit=AsyncMock(), rollback=AsyncMock())
|
|
redis = SimpleNamespace(set=AsyncMock(return_value=True), xack=AsyncMock(), delete=AsyncMock())
|
|
file_access_log = make_file_access_log()
|
|
messages = [
|
|
(
|
|
'1-0',
|
|
{
|
|
'event_type': 'file_access',
|
|
'event_id': 'event-id',
|
|
'payload': json.dumps(file_access_log.model_dump(by_alias=True, exclude_none=True), default=str),
|
|
},
|
|
)
|
|
]
|
|
|
|
with (
|
|
patch(
|
|
'module_admin.service.log_service.AsyncSessionLocal',
|
|
return_value=AsyncSessionContext(session),
|
|
),
|
|
patch.object(FileAccessLogDao, 'add_file_access_log_dao', new_callable=AsyncMock) as add_file_access_log,
|
|
):
|
|
asyncio.run(LogAggregatorService._process_messages(redis, LogConfig.log_stream_key, messages))
|
|
|
|
add_file_access_log.assert_awaited_once()
|
|
saved_log = add_file_access_log.await_args.args[1]
|
|
assert saved_log.file_id == 'file-id'
|
|
assert saved_log.result == 'completed'
|
|
assert saved_log.operation_detail == '{"newStatus":"active"}'
|
|
session.commit.assert_awaited_once()
|
|
redis.xack.assert_awaited_once_with(LogConfig.log_stream_key, LogConfig.log_stream_group, '1-0')
|
|
|
|
|
|
def test_file_audit_batch_events_use_independent_deduplication_keys() -> None:
|
|
redis = SimpleNamespace(xadd=AsyncMock())
|
|
request = SimpleNamespace(
|
|
app=SimpleNamespace(state=SimpleNamespace(redis=redis)),
|
|
headers={'User-Agent': 'pytest'},
|
|
client=SimpleNamespace(host='127.0.0.1'),
|
|
)
|
|
current_user = SimpleNamespace(user=SimpleNamespace(user_id=10, user_name='user10'))
|
|
|
|
async def enqueue_batch() -> None:
|
|
await FileAuditService.enqueue_file_audit(
|
|
request,
|
|
current_user,
|
|
'file-id-1',
|
|
'transfer',
|
|
'completed',
|
|
operation_detail={'previousOwnerUserId': 10, 'newOwnerUserId': 20},
|
|
)
|
|
await FileAuditService.enqueue_file_audit(
|
|
request,
|
|
current_user,
|
|
'file-id-2',
|
|
'transfer',
|
|
'completed',
|
|
operation_detail={'previousOwnerUserId': 10, 'newOwnerUserId': 20},
|
|
)
|
|
|
|
with (
|
|
patch('module_admin.service.file_access_service.TraceCtx.get_request_id', return_value='request-id'),
|
|
patch('module_admin.service.file_access_service.TraceCtx.get_trace_id', return_value='trace-id'),
|
|
patch('module_admin.service.log_service.TraceCtx.get_request_id', return_value='request-id'),
|
|
patch('module_admin.service.log_service.TraceCtx.get_trace_id', return_value='trace-id'),
|
|
patch('module_admin.service.log_service.TraceCtx.get_span_id', return_value='span-id'),
|
|
):
|
|
asyncio.run(enqueue_batch())
|
|
|
|
assert redis.xadd.await_count == BATCH_FILE_COUNT
|
|
first_event = redis.xadd.await_args_list[0].args[1]
|
|
second_event = redis.xadd.await_args_list[1].args[1]
|
|
assert first_event['event_id'] != second_event['event_id']
|
|
assert json.loads(first_event['payload'])['operationDetail'] == ('{"previousOwnerUserId":10,"newOwnerUserId":20}')
|
|
|
|
|
|
def test_file_audit_operation_detail_is_not_truncated() -> None:
|
|
reason = 'a' * LONG_OPERATION_DETAIL_LENGTH
|
|
serialized_detail = FileAuditService._serialize_operation_detail({'reason': reason})
|
|
|
|
parsed_detail = json.loads(serialized_detail)
|
|
assert parsed_detail['reason'] == reason
|