Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/tests/module_admin/service/test_file_reference.py
T
insistence 2a055ba648 feat: 新增插件系统 (#112)
* 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: 更新后端依赖文件
2026-07-28 20:35:18 +08:00

288 lines
8.8 KiB
Python

import asyncio
from datetime import datetime, timedelta
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
import pytest
from sqlalchemy import true
from exceptions.exception import ServiceException
from module_admin.dao.file_business_dao import FileReferenceDao
from module_admin.dao.file_info_dao import FileInfoDao
from module_admin.entity.do.file_do import SysFileReference
from module_admin.entity.vo.file_vo import FileRetentionPolicyModel
from module_admin.service.file_business_service import FileReferenceService, FileRetentionPolicyService
FILE_ID = '11111111-1111-4111-8111-111111111111'
def make_query_db() -> SimpleNamespace:
return SimpleNamespace(commit=AsyncMock(), rollback=AsyncMock())
def test_get_file_reference_list_includes_legacy_reference() -> None:
file_info = {
'file_id': FILE_ID,
'business_type': 'notice',
'business_id': '10',
}
with (
patch.object(
FileInfoDao,
'get_file_management_detail_by_id',
new=AsyncMock(return_value=file_info),
),
patch.object(FileReferenceDao, 'get_file_reference_list', new=AsyncMock(return_value=[])),
):
result = asyncio.run(
FileReferenceService.get_file_reference_list_services(
make_query_db(),
FILE_ID,
true(),
)
)
assert len(result) == 1
assert result[0].business_type == 'notice'
assert result[0].business_id == '10'
assert result[0].legacy is True
def test_get_file_reference_list_converts_orm_reference() -> None:
create_time = datetime(2026, 7, 26, 10, 0, 0)
file_info = {
'file_id': FILE_ID,
'business_type': None,
'business_id': None,
}
file_reference = SysFileReference(
reference_id=1,
file_id=FILE_ID,
business_type='contract',
business_id='100',
business_name='Contract',
create_by='admin',
create_time=create_time,
)
with (
patch.object(
FileInfoDao,
'get_file_management_detail_by_id',
new=AsyncMock(return_value=file_info),
),
patch.object(
FileReferenceDao,
'get_file_reference_list',
new=AsyncMock(return_value=[file_reference]),
),
):
result = asyncio.run(
FileReferenceService.get_file_reference_list_services(
make_query_db(),
FILE_ID,
true(),
)
)
assert result[0].model_dump(by_alias=True) == {
'referenceId': 1,
'fileId': FILE_ID,
'businessType': 'contract',
'businessId': '100',
'businessName': 'Contract',
'retentionExpireTime': None,
'createBy': 'admin',
'createTime': create_time,
'legacy': False,
}
def test_replace_business_file_references_locks_files_without_committing() -> None:
query_db = make_query_db()
file_infos = [SimpleNamespace(file_id=FILE_ID)]
with (
patch.object(
FileInfoDao,
'get_file_infos_by_ids_for_update',
new=AsyncMock(return_value=file_infos),
) as get_file_infos,
patch.object(
FileReferenceDao,
'replace_business_file_references',
new_callable=AsyncMock,
) as replace_references,
patch.object(
FileRetentionPolicyService,
'get_enabled_file_retention_policy_services',
new=AsyncMock(return_value=None),
),
):
asyncio.run(
FileReferenceService.replace_business_file_references_services(
query_db,
'notice',
'10',
[FILE_ID, FILE_ID],
create_by='admin',
file_data_scope_sql=true(),
business_name='系统公告',
)
)
assert get_file_infos.await_args.args[1] == [FILE_ID]
reference_list = replace_references.await_args.args[3]
assert len(reference_list) == 1
assert isinstance(reference_list[0], SysFileReference)
assert reference_list[0].file_id == FILE_ID
assert reference_list[0].business_type == 'notice'
assert reference_list[0].business_id == '10'
assert reference_list[0].business_name == '系统公告'
query_db.commit.assert_not_awaited()
def test_replace_business_file_references_rejects_invalid_file() -> None:
query_db = make_query_db()
with (
patch.object(FileInfoDao, 'get_file_infos_by_ids_for_update', new=AsyncMock(return_value=[])),
patch.object(
FileReferenceDao,
'replace_business_file_references',
new_callable=AsyncMock,
) as replace_references,
pytest.raises(ServiceException) as file_error,
):
asyncio.run(
FileReferenceService.replace_business_file_references_services(
query_db,
'notice',
'10',
[FILE_ID],
create_by='admin',
file_data_scope_sql=true(),
)
)
assert file_error.value.message == '部分引用文件不存在或已失效'
replace_references.assert_not_awaited()
def test_remove_business_file_references_does_not_lock_files() -> None:
query_db = make_query_db()
with (
patch.object(
FileReferenceDao,
'replace_business_file_references',
new_callable=AsyncMock,
) as replace_references,
patch.object(
FileRetentionPolicyService,
'get_enabled_file_retention_policy_services',
new=AsyncMock(return_value=None),
),
):
asyncio.run(
FileReferenceService.remove_business_file_references_services(
query_db,
'notice',
'10',
)
)
assert replace_references.await_args.args[3] == []
query_db.commit.assert_not_awaited()
def test_replace_business_file_references_applies_retention_policy() -> None:
query_db = make_query_db()
create_time = datetime(2026, 7, 23, 10, 0, 0)
policy = FileRetentionPolicyModel(businessType='notice', retentionDays=30)
with (
patch.object(
FileInfoDao,
'get_file_infos_by_ids_for_update',
new=AsyncMock(return_value=[SimpleNamespace(file_id=FILE_ID)]),
),
patch.object(
FileRetentionPolicyService,
'get_enabled_file_retention_policy_services',
new=AsyncMock(return_value=policy),
),
patch.object(
FileReferenceDao,
'replace_business_file_references',
new_callable=AsyncMock,
) as replace_references,
patch(
'module_admin.service.file_business_service.datetime',
new=SimpleNamespace(now=lambda: create_time),
),
):
asyncio.run(
FileReferenceService.replace_business_file_references_services(
query_db,
'notice',
'10',
[FILE_ID],
create_by='admin',
file_data_scope_sql=true(),
)
)
reference = replace_references.await_args.args[3][0]
assert reference.retention_expire_time == create_time + timedelta(days=30)
def test_replace_business_file_references_preserves_extended_expiration() -> None:
policy_expire_time = datetime(2026, 8, 22, 10, 0, 0)
extended_expire_time = datetime(2027, 7, 23, 10, 0, 0)
old_reference = SysFileReference(
file_id=FILE_ID,
business_type='notice',
business_id='10',
retention_expire_time=extended_expire_time,
)
new_reference = SysFileReference(
file_id=FILE_ID,
business_type='notice',
business_id='10',
retention_expire_time=policy_expire_time,
)
FileReferenceDao._preserve_later_retention_expire_times(
[old_reference],
[new_reference],
)
assert new_reference.retention_expire_time == extended_expire_time
def test_refresh_file_expire_times_uses_latest_reference_expiration() -> None:
first_expire_time = datetime(2026, 8, 1)
last_expire_time = datetime(2026, 9, 1)
file_info = SimpleNamespace(
file_id=FILE_ID,
business_type=None,
business_id=None,
expire_time=None,
)
query_db = SimpleNamespace(
execute=AsyncMock(
return_value=SimpleNamespace(
all=lambda: [
(FILE_ID, first_expire_time),
(FILE_ID, last_expire_time),
]
)
)
)
asyncio.run(
FileReferenceDao._refresh_file_expire_times(
query_db,
[FILE_ID],
{FILE_ID: file_info},
)
)
assert file_info.expire_time == last_expire_time