Files
insistence 726883f68a feat: 新增跨时区支持并完善定时任务调度 (#127)
* feat: 新增跨时区支持

* perf: 优化代码

* fix: 修复测试

* perf: 优化调度器日志打印
2026-09-12 17:01:05 +08:00

285 lines
8.9 KiB
Python

import asyncio
from datetime import datetime, timedelta, timezone
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, tzinfo=timezone.utc)
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, tzinfo=timezone.utc)
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.TimezoneUtil.utc_now', return_value=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, tzinfo=timezone.utc)
extended_expire_time = datetime(2027, 7, 23, 10, 0, 0, tzinfo=timezone.utc)
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, tzinfo=timezone.utc)
last_expire_time = datetime(2026, 9, 1, tzinfo=timezone.utc)
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