| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- """
- 费控制度模块专题测试。
- 解决策略:
- 1. 函数内 lazy import 避免模块级依赖
- 2. patch.object 对已导入类的实例方法打桩
- 3. patch.object 对导入后的模块全局变量打桩(绕过 @patch 字符串路径限制)
- """
- import json
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- pytestmark = pytest.mark.asyncio
- # ====================== 测试数据 ======================
- MOCK_INSTITUTION_ID = "inst_202605150001"
- MOCK_ENTERPRISE_ID = "ent_202605150001"
- MOCK_ISSUE_RULE_ID = "rule_202605150001"
- @pytest.fixture
- def mock_auth():
- """模拟认证上下文"""
- auth = MagicMock()
- auth.user.id = 1
- auth.tenant_id = "tenant_001"
- auth.db = AsyncMock()
- return auth
- # ====================== 费控制度 Service 测试 ======================
- class TestInstitutionCreate:
- """测试创建费控制度的串联流程"""
- async def test_create_full_flow_success(self, mock_auth):
- """正向场景:完整的创建流程(create + scope + issuerule + 本地保存)"""
- import app.plugin.module_payment.expense.institution.service as svc
- inst = svc.InstitutionService
- scope_svc = svc.InstitutionScopeService
- issuerule_svc = svc.IssueruleService
- with (
- patch.object(inst, "create_institution_service") as mock_create_inst,
- patch.object(scope_svc, "scope_modify_service") as mock_scope,
- patch.object(issuerule_svc, "create_issuerule_service") as mock_issuerule,
- patch.object(svc, "InstitutionCRUD") as mock_crud_cls,
- ):
- mock_create_inst.return_value = MagicMock(institution_id=MOCK_INSTITUTION_ID)
- mock_scope.return_value = {"result": True}
- mock_issuerule.return_value = {"issue_rule_id": MOCK_ISSUE_RULE_ID}
- mock_crud_instance = AsyncMock()
- mock_crud_cls.return_value = mock_crud_instance
- from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
- AlipayEbppInvoiceInstitutionCreateModel,
- )
- model = AlipayEbppInvoiceInstitutionCreateModel()
- model.enterprise_id = MOCK_ENTERPRISE_ID
- model.institution_name = "测试制度"
- model.effective = "1"
- result = await inst.create_institution_full_flow(
- auth=mock_auth,
- institution_model=model,
- enterprise_id=MOCK_ENTERPRISE_ID,
- scope_data={"adapter_type": "EMPLOYEE_ALL"},
- issuerule_data={
- "quota_type": "CAP",
- "issue_type": "ISSUE_MONTH",
- "issue_amount_value": "1000",
- "issue_rule_name": "测试制度-发放规则",
- },
- )
- assert result["institution_id"] == MOCK_INSTITUTION_ID
- assert result["scope_modified"] is True
- assert result["issue_rule_id"] == MOCK_ISSUE_RULE_ID
- mock_create_inst.assert_called_once()
- mock_scope.assert_called_once()
- mock_issuerule.assert_called_once()
- mock_crud_instance.create.assert_called_once()
- async def test_create_without_scope_and_issuerule(self, mock_auth):
- """边界场景:不传 scope 和 issuerule 时只调 create"""
- import app.plugin.module_payment.expense.institution.service as svc
- inst = svc.InstitutionService
- scope_svc = svc.InstitutionScopeService
- issuerule_svc = svc.IssueruleService
- with (
- patch.object(inst, "create_institution_service") as mock_create_inst,
- patch.object(scope_svc, "scope_modify_service") as mock_scope,
- patch.object(issuerule_svc, "create_issuerule_service") as mock_issuerule,
- patch.object(svc, "InstitutionCRUD") as mock_crud_cls,
- ):
- mock_create_inst.return_value = MagicMock(institution_id=MOCK_INSTITUTION_ID)
- mock_crud_instance = AsyncMock()
- mock_crud_cls.return_value = mock_crud_instance
- from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
- AlipayEbppInvoiceInstitutionCreateModel,
- )
- model = AlipayEbppInvoiceInstitutionCreateModel()
- model.enterprise_id = MOCK_ENTERPRISE_ID
- model.institution_name = "无成员无规则制度"
- result = await inst.create_institution_full_flow(
- auth=mock_auth,
- institution_model=model,
- enterprise_id=MOCK_ENTERPRISE_ID,
- scope_data=None,
- issuerule_data=None,
- )
- assert result["institution_id"] == MOCK_INSTITUTION_ID
- assert result["scope_modified"] is False
- assert result["issue_rule_id"] is None
- mock_scope.assert_not_called()
- mock_issuerule.assert_not_called()
- async def test_create_rollback_on_failure(self, mock_auth):
- """异常场景:scope 失败后触发回滚(删除已创建的制度)"""
- import app.plugin.module_payment.expense.institution.service as svc
- inst = svc.InstitutionService
- scope_svc = svc.InstitutionScopeService
- with (
- patch.object(inst, "create_institution_service") as mock_create_inst,
- patch.object(scope_svc, "scope_modify_service") as mock_scope,
- ):
- mock_create_inst.return_value = MagicMock(institution_id=MOCK_INSTITUTION_ID)
- mock_scope.side_effect = Exception("支付宝接口异常")
- with patch("asyncio.to_thread") as mock_to_thread:
- from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
- AlipayEbppInvoiceInstitutionCreateModel,
- )
- model = AlipayEbppInvoiceInstitutionCreateModel()
- model.enterprise_id = MOCK_ENTERPRISE_ID
- model.institution_name = "测试回滚制度"
- with pytest.raises(Exception, match="支付宝接口异常"):
- await inst.create_institution_full_flow(
- auth=mock_auth,
- institution_model=model,
- enterprise_id=MOCK_ENTERPRISE_ID,
- scope_data={"adapter_type": "EMPLOYEE_ALL"},
- issuerule_data=None,
- )
- assert mock_to_thread.called, "回滚时应该调用了 asyncio.to_thread"
- class TestInstitutionDelete:
- """测试删除费控制度"""
- async def test_delete_sync_local_db(self, mock_auth):
- """正向场景:删除成功后同步删除本地记录"""
- import app.plugin.module_payment.expense.institution.service as svc
- from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionDeleteModel import (
- AlipayEbppInvoiceInstitutionDeleteModel,
- )
- inst = svc.InstitutionService
- with (
- patch.object(inst, "_execute_alipay") as mock_execute,
- patch.object(svc, "InstitutionCRUD") as mock_crud_cls,
- ):
- mock_execute.return_value = json.dumps({
- "alipay_ebpp_invoice_institution_delete_response": {
- "code": "10000", "msg": "Success",
- }
- })
- mock_obj = MagicMock()
- mock_obj.id = 1
- mock_crud_instance = AsyncMock()
- mock_crud_instance.get = AsyncMock(return_value=mock_obj)
- mock_crud_cls.return_value = mock_crud_instance
- delete_model = AlipayEbppInvoiceInstitutionDeleteModel()
- delete_model.institution_id = MOCK_INSTITUTION_ID
- delete_model.enterprise_id = MOCK_ENTERPRISE_ID
- result = await inst.delete_institution_service(auth=mock_auth, data=delete_model)
- assert result is not None
- mock_crud_instance.get.assert_called_with(institution_id=MOCK_INSTITUTION_ID)
- mock_crud_instance.delete.assert_called_with(ids=[mock_obj.id])
- class TestIssueruleConstraint:
- """测试自动发放规则的参数约束"""
- async def test_cap_must_be_accumulative(self, mock_auth):
- """余额类型(CAP)必须为可累计(invalid_mode=1)"""
- from app.core.exceptions import CustomException
- import app.plugin.module_payment.expense.institution.service as svc
- with patch.object(svc.IssueruleService, "_execute_alipay") as mock_execute:
- with pytest.raises(CustomException, match="余额类型.*必须为可累计"):
- await svc.IssueruleService.create_issuerule_service(
- auth=mock_auth,
- institution_id=MOCK_INSTITUTION_ID,
- enterprise_id=MOCK_ENTERPRISE_ID,
- quota_type="CAP",
- issue_type="ISSUE_MONTH",
- issue_amount_value="500",
- invalid_mode=0,
- )
- mock_execute.assert_not_called()
- async def test_count_cannot_transfer(self, mock_auth):
- """次卡类型(COUNT)不可转赠(share_mode=0)"""
- from app.core.exceptions import CustomException
- import app.plugin.module_payment.expense.institution.service as svc
- with patch.object(svc.IssueruleService, "_execute_alipay") as mock_execute:
- with pytest.raises(CustomException, match="次卡类型.*不可转赠"):
- await svc.IssueruleService.create_issuerule_service(
- auth=mock_auth,
- institution_id=MOCK_INSTITUTION_ID,
- enterprise_id=MOCK_ENTERPRISE_ID,
- quota_type="COUNT",
- issue_type="ISSUE_MONTH",
- issue_amount_value="10",
- share_mode=1,
- )
- mock_execute.assert_not_called()
- # ====================== Scope Sync 测试 ======================
- class TestScopeSync:
- """测试员工/部门停用联动"""
- async def test_remove_employee(self, mock_auth):
- """解约员工时:从所有引用该员工的制度中移除"""
- import app.plugin.module_payment.expense.institution.scope_sync as sync
- import app.plugin.module_payment.expense.institution.service as svc
- with (
- patch.object(sync, "InstitutionCRUD") as mock_crud_cls,
- patch.object(svc.InstitutionScopeService, "scope_modify_service") as mock_scope_modify,
- ):
- mock_inst_1 = MagicMock()
- mock_inst_1.institution_id = "inst_001"
- mock_inst_2 = MagicMock()
- mock_inst_2.institution_id = "inst_002"
- mock_crud_instance = AsyncMock()
- mock_crud_instance.list = AsyncMock(return_value=[mock_inst_1, mock_inst_2])
- mock_crud_cls.return_value = mock_crud_instance
- await sync.remove_employee_from_institution_scopes(
- auth=mock_auth,
- enterprise_id=MOCK_ENTERPRISE_ID,
- employee_id="emp_001",
- )
- assert mock_scope_modify.call_count == 2
- mock_scope_modify.assert_any_call(
- auth=mock_auth,
- institution_id="inst_001",
- data={
- "enterprise_id": MOCK_ENTERPRISE_ID,
- "adapter_type": "EMPLOYEE_SELECT",
- "delete_owner_id_list": ["emp_001"],
- },
- )
- async def test_remove_department(self, mock_auth):
- """停用部门时:从所有引用该部门的制度中移除"""
- import app.plugin.module_payment.expense.institution.scope_sync as sync
- import app.plugin.module_payment.expense.institution.service as svc
- with (
- patch.object(sync, "InstitutionCRUD") as mock_crud_cls,
- patch.object(svc.InstitutionScopeService, "scope_modify_service") as mock_scope_modify,
- ):
- mock_inst = MagicMock()
- mock_inst.institution_id = "inst_001"
- mock_crud_instance = AsyncMock()
- mock_crud_instance.list = AsyncMock(return_value=[mock_inst])
- mock_crud_cls.return_value = mock_crud_instance
- await sync.remove_department_from_institution_scopes(
- auth=mock_auth,
- enterprise_id=MOCK_ENTERPRISE_ID,
- department_id="dept_001",
- )
- mock_scope_modify.assert_called_once_with(
- auth=mock_auth,
- institution_id="inst_001",
- data={
- "enterprise_id": MOCK_ENTERPRISE_ID,
- "adapter_type": "EMPLOYEE_DEPARTMENT",
- "delete_owner_id_list": ["dept_001"],
- },
- )
|