Close the current assignee's task as transferred and create a same-node pending task for the target user; include transferred in done lists. Co-authored-by: Cursor <cursoragent@cursor.com>
1413 lines
46 KiB
Python
1413 lines
46 KiB
Python
"""OA 引擎单元测试(纯逻辑 + 轻量 mock)"""
|
||
import json
|
||
import unittest
|
||
from datetime import datetime
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
from app.services.oa_engine import (
|
||
OaEngineError,
|
||
_find_node,
|
||
_flow_node_key_order,
|
||
_match_condition_group,
|
||
_sort_tasks_for_detail,
|
||
advance_from,
|
||
)
|
||
|
||
|
||
class TestOaEngineHelpers(unittest.TestCase):
|
||
def test_find_node(self):
|
||
root = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
}
|
||
self.assertEqual(_find_node(root, "a1")["id"], "a1")
|
||
self.assertEqual(_find_node(root, "e1")["id"], "e1")
|
||
self.assertIsNone(_find_node(root, "x"))
|
||
|
||
def test_condition_match(self):
|
||
groups = [
|
||
{
|
||
"name": "gt3",
|
||
"when": "{days} > 3",
|
||
"child": {"id": "n1", "type": "end"},
|
||
},
|
||
{"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
|
||
]
|
||
g = _match_condition_group(groups, {"days": 5})
|
||
self.assertEqual(g["name"], "gt3")
|
||
g2 = _match_condition_group(groups, {"days": 1})
|
||
self.assertEqual(g2["name"], "default")
|
||
|
||
def test_condition_match_rules_list(self):
|
||
groups = [
|
||
{
|
||
"name": "gt3",
|
||
"rules": [{"field": "days", "op": ">", "value": 3}],
|
||
"child": {"id": "n1", "type": "end"},
|
||
},
|
||
{"name": "default", "isDefault": True, "rules": [], "child": {"id": "n2", "type": "end"}},
|
||
]
|
||
self.assertEqual(_match_condition_group(groups, {"days": 5})["name"], "gt3")
|
||
self.assertEqual(_match_condition_group(groups, {"days": 1})["name"], "default")
|
||
|
||
def test_condition_match_rules_and(self):
|
||
groups = [
|
||
{
|
||
"name": "both",
|
||
"rules": [
|
||
{"field": "days", "op": ">", "value": 3},
|
||
{"field": "leaveType", "op": "==", "value": "事假"},
|
||
],
|
||
"child": {"id": "n1", "type": "end"},
|
||
},
|
||
{"name": "default", "rules": [], "isDefault": True, "child": {"id": "n2", "type": "end"}},
|
||
]
|
||
self.assertEqual(
|
||
_match_condition_group(groups, {"days": 5, "leaveType": "事假"})["name"],
|
||
"both",
|
||
)
|
||
self.assertEqual(
|
||
_match_condition_group(groups, {"days": 5, "leaveType": "病假"})["name"],
|
||
"default",
|
||
)
|
||
|
||
def test_flow_node_key_order_cc_before_approval(self):
|
||
root = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "c1",
|
||
"type": "cc",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
},
|
||
}
|
||
order = _flow_node_key_order(root)
|
||
self.assertLess(order["c1"], order["a1"])
|
||
|
||
def test_sort_tasks_by_flow_when_same_create_time(self):
|
||
"""抄送先于审批设计时,即使 create_time / order_num 相同也应按树序展示。"""
|
||
root = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "c1",
|
||
"type": "cc",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
},
|
||
}
|
||
ts = datetime(2026, 7, 15, 14, 5, 7)
|
||
approval = MagicMock(node_key="a1", order_num=0, create_time=ts, id="AAAA")
|
||
cc = MagicMock(node_key="c1", order_num=0, create_time=ts, id="BBBB")
|
||
# 故意把审批放在列表前面,模拟 create_time 同秒时 PK 乱序
|
||
sorted_tasks = _sort_tasks_for_detail([approval, cc], flow_root=root)
|
||
self.assertEqual([t.node_key for t in sorted_tasks], ["c1", "a1"])
|
||
|
||
|
||
class TestAdvanceFrom(unittest.TestCase):
|
||
def test_linear_creates_approval_then_stops(self):
|
||
flow = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"name": "负责人",
|
||
"approverConfig": {"assigneeType": "dept_leader"},
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
}
|
||
db = MagicMock()
|
||
inst = MagicMock(
|
||
id="inst1",
|
||
tenant_no="zkxw",
|
||
initiator_user_id="u0",
|
||
initiator_dept_id="d1",
|
||
form_data_json="{}",
|
||
status="running",
|
||
)
|
||
ver = MagicMock(
|
||
flow_tree_json=json.dumps(flow),
|
||
advanced_settings_json=json.dumps({"supervisorFallbackToDeptLeader": True}),
|
||
)
|
||
fake_task = MagicMock()
|
||
with patch(
|
||
"app.services.oa_engine._next_task_order_num", return_value=1
|
||
), patch(
|
||
"app.services.oa_engine._create_tasks_for_node",
|
||
return_value=[fake_task],
|
||
) as mock_create:
|
||
advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
|
||
mock_create.assert_called_once()
|
||
self.assertEqual(inst.status, "running")
|
||
self.assertEqual(mock_create.call_args.kwargs.get("order_num_start"), 1)
|
||
|
||
def test_complete_path_to_end_via_cc(self):
|
||
flow = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "c1",
|
||
"type": "cc",
|
||
"name": "抄送",
|
||
"approverConfig": {"assigneeType": "users", "userIds": ["u9"]},
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
}
|
||
db = MagicMock()
|
||
inst = MagicMock(
|
||
id="inst1",
|
||
tenant_no="zkxw",
|
||
initiator_user_id="u0",
|
||
initiator_dept_id="d1",
|
||
form_data_json="{}",
|
||
status="running",
|
||
update_time=datetime.now(),
|
||
)
|
||
ver = MagicMock(
|
||
flow_tree_json=json.dumps(flow),
|
||
advanced_settings_json="{}",
|
||
)
|
||
with patch(
|
||
"app.services.oa_engine._next_task_order_num", return_value=1
|
||
), patch(
|
||
"app.services.oa_engine._create_tasks_for_node",
|
||
return_value=[MagicMock()],
|
||
):
|
||
advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
|
||
self.assertEqual(inst.status, "completed")
|
||
|
||
def test_empty_assignee_raises(self):
|
||
flow = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"approverConfig": {"assigneeType": "dept_leader"},
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
}
|
||
db = MagicMock()
|
||
inst = MagicMock(
|
||
id="inst1",
|
||
tenant_no="zkxw",
|
||
initiator_user_id="u0",
|
||
initiator_dept_id="d1",
|
||
form_data_json="{}",
|
||
status="running",
|
||
)
|
||
ver = MagicMock(flow_tree_json=json.dumps(flow), advanced_settings_json="{}")
|
||
with patch(
|
||
"app.services.oa_engine._next_task_order_num", return_value=1
|
||
), patch("app.services.oa_engine._create_tasks_for_node", return_value=[]):
|
||
with self.assertRaises(OaEngineError):
|
||
advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
|
||
|
||
|
||
class TestAddSign(unittest.TestCase):
|
||
def test_add_sign_rejects_when_disabled(self):
|
||
from app.services.oa_engine import add_sign
|
||
|
||
db = MagicMock()
|
||
task = MagicMock(
|
||
id="t1",
|
||
assignee_user_id="u1",
|
||
task_type="approval",
|
||
status="pending",
|
||
instance_id="i1",
|
||
node_key="a1",
|
||
node_name="审批",
|
||
sign_mode="or",
|
||
comment_text=None,
|
||
)
|
||
inst = MagicMock(id="i1", status="running", initiator_dept_id="d1")
|
||
ver = MagicMock(
|
||
id="v1",
|
||
advanced_settings_json=json.dumps({"allowAddSign": False}),
|
||
del_flag=0,
|
||
)
|
||
db.query.return_value.filter.return_value.first.side_effect = [task, inst, ver]
|
||
with self.assertRaises(OaEngineError) as ctx:
|
||
add_sign(
|
||
db,
|
||
"t1",
|
||
tenant_no="zkxw",
|
||
user_id="u1",
|
||
user_ids=["u2"],
|
||
)
|
||
self.assertIn("不允许加签", str(ctx.exception))
|
||
|
||
def test_add_sign_requires_users(self):
|
||
from app.services.oa_engine import add_sign
|
||
|
||
db = MagicMock()
|
||
task = MagicMock(
|
||
id="t1",
|
||
assignee_user_id="u1",
|
||
task_type="approval",
|
||
status="pending",
|
||
instance_id="i1",
|
||
)
|
||
inst = MagicMock(id="i1", status="running")
|
||
ver = MagicMock(advanced_settings_json="{}", del_flag=0)
|
||
db.query.return_value.filter.return_value.first.side_effect = [task, inst, ver]
|
||
with self.assertRaises(OaEngineError) as ctx:
|
||
add_sign(db, "t1", tenant_no="zkxw", user_id="u1", user_ids=[])
|
||
self.assertIn("加签人", str(ctx.exception))
|
||
|
||
|
||
class TestCompleteTaskOrSign(unittest.TestCase):
|
||
"""回归:Session autoflush=False 时,或签取消同节点待办不可覆盖本人已同意状态。"""
|
||
|
||
def setUp(self):
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
from app.core.database import Base
|
||
from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
|
||
|
||
self.engine = create_engine("sqlite:///:memory:")
|
||
# 仅建引擎相关表
|
||
OaApp.__table__.create(self.engine)
|
||
OaAppVersion.__table__.create(self.engine)
|
||
OaInstance.__table__.create(self.engine)
|
||
OaTask.__table__.create(self.engine)
|
||
OaInstanceEvent.__table__.create(self.engine)
|
||
SysUser.__table__.create(self.engine)
|
||
Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
||
self.db = Session()
|
||
self.Base = Base
|
||
self.OaAppVersion = OaAppVersion
|
||
self.OaInstance = OaInstance
|
||
self.OaTask = OaTask
|
||
|
||
flow = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"name": "审批人",
|
||
"approverConfig": {"signMode": "or"},
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
}
|
||
now = datetime.now()
|
||
self.db.add(
|
||
OaAppVersion(
|
||
id="v1",
|
||
app_id="app1",
|
||
version_no=1,
|
||
status="published",
|
||
is_latest_published="1",
|
||
form_schema_json="{}",
|
||
flow_tree_json=json.dumps(flow),
|
||
advanced_settings_json="{}",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaInstance(
|
||
id="i1",
|
||
app_id="app1",
|
||
app_version_id="v1",
|
||
title="单人审批",
|
||
initiator_user_id="u0",
|
||
initiator_dept_id="d1",
|
||
form_data_json="{}",
|
||
status="running",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaTask(
|
||
id="t1",
|
||
instance_id="i1",
|
||
node_key="a1",
|
||
node_name="审批人",
|
||
task_type="approval",
|
||
assignee_user_id="u1",
|
||
status="pending",
|
||
sign_mode="or",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.commit()
|
||
|
||
def tearDown(self):
|
||
self.db.close()
|
||
self.engine.dispose()
|
||
|
||
def test_approve_keeps_approved_not_done(self):
|
||
from app.services.oa_engine import complete_task
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "completed"},
|
||
):
|
||
complete_task(
|
||
self.db,
|
||
"t1",
|
||
tenant_no="zkxw",
|
||
user_id="u1",
|
||
action="approve",
|
||
comment="同意",
|
||
)
|
||
|
||
task = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
|
||
self.assertEqual(task.status, "approved")
|
||
self.assertEqual(task.comment_text, "同意")
|
||
|
||
# 已处理列表应能查到
|
||
from app.services.oa_engine import list_tasks
|
||
|
||
done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
|
||
self.assertEqual(len(done), 1)
|
||
self.assertEqual(done[0]["status"], "approved")
|
||
|
||
def test_or_sign_cancels_siblings_only(self):
|
||
from app.services.oa_engine import complete_task
|
||
|
||
now = datetime.now()
|
||
self.db.add(
|
||
self.OaTask(
|
||
id="t2",
|
||
instance_id="i1",
|
||
node_key="a1",
|
||
node_name="审批人",
|
||
task_type="approval",
|
||
assignee_user_id="u2",
|
||
status="pending",
|
||
sign_mode="or",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.commit()
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "completed"},
|
||
):
|
||
complete_task(
|
||
self.db,
|
||
"t1",
|
||
tenant_no="zkxw",
|
||
user_id="u1",
|
||
action="approve",
|
||
comment="同意",
|
||
)
|
||
|
||
t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
|
||
t2 = self.db.query(self.OaTask).filter(self.OaTask.id == "t2").first()
|
||
self.assertEqual(t1.status, "approved")
|
||
self.assertEqual(t2.status, "done")
|
||
|
||
|
||
class TestBuildProgress(unittest.TestCase):
|
||
def test_cc_before_approval_and_pending_ahead(self):
|
||
from app.services.oa_engine import build_progress_items
|
||
|
||
root = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "c1",
|
||
"type": "cc",
|
||
"name": "抄送人",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"name": "审批人",
|
||
"child": {
|
||
"id": "a2",
|
||
"type": "approval",
|
||
"name": "总经理",
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
ts = datetime(2026, 7, 15, 14, 5, 7)
|
||
# 审批任务 create 同秒但列表故意审批在前
|
||
t_approval = MagicMock(
|
||
id="tA",
|
||
node_key="a1",
|
||
node_name="审批人",
|
||
task_type="approval",
|
||
status="pending",
|
||
assignee_user_id="u2",
|
||
order_num=0,
|
||
round_no=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
comment_text=None,
|
||
resolve_source="users",
|
||
)
|
||
t_cc = MagicMock(
|
||
id="tC",
|
||
node_key="c1",
|
||
node_name="抄送人",
|
||
task_type="cc",
|
||
status="done",
|
||
assignee_user_id="u9",
|
||
order_num=0,
|
||
round_no=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
comment_text=None,
|
||
resolve_source="users",
|
||
)
|
||
inst = MagicMock(
|
||
status="running",
|
||
initiator_user_id="u0",
|
||
current_round=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
)
|
||
items = build_progress_items(
|
||
inst=inst,
|
||
flow_root=root,
|
||
tasks=[t_approval, t_cc],
|
||
name_map={"u0": "发起人甲", "u2": "审批乙", "u9": "抄送丙"},
|
||
events=[], # P0 可空
|
||
)
|
||
kinds = [x["kind"] for x in items]
|
||
self.assertEqual(kinds[0], "submit")
|
||
# 已产生:cc 再 approval
|
||
self.assertEqual(
|
||
[x["nodeKey"] for x in items if x["kind"] == "task"],
|
||
["c1", "a1"],
|
||
)
|
||
# running 时未到达 a2
|
||
pending = [x for x in items if x["kind"] == "pending_node"]
|
||
self.assertTrue(any(p["nodeKey"] == "a2" for p in pending))
|
||
|
||
def test_running_shows_all_subsequent_pending_nodes(self):
|
||
"""进行中应挂出路径上全部后续节点,而非只露一个空壳。"""
|
||
from app.services.oa_engine import build_progress_items
|
||
|
||
root = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"name": "一级审批",
|
||
"child": {
|
||
"id": "a2",
|
||
"type": "approval",
|
||
"name": "二级审批",
|
||
"child": {
|
||
"id": "a3",
|
||
"type": "approval",
|
||
"name": "三级审批",
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
ts = datetime(2026, 7, 17, 11, 0, 0)
|
||
t1 = MagicMock(
|
||
id="t1",
|
||
node_key="a1",
|
||
node_name="一级审批",
|
||
task_type="approval",
|
||
status="pending",
|
||
assignee_user_id="u1",
|
||
order_num=0,
|
||
round_no=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
comment_text=None,
|
||
resolve_source="users",
|
||
sign_mode="or",
|
||
)
|
||
inst = MagicMock(
|
||
status="running",
|
||
initiator_user_id="u0",
|
||
current_round=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
form_data_json="{}",
|
||
tenant_no="t1",
|
||
initiator_dept_id="",
|
||
)
|
||
items = build_progress_items(
|
||
inst=inst,
|
||
flow_root=root,
|
||
tasks=[t1],
|
||
name_map={"u0": "发起人", "u1": "甲"},
|
||
events=[],
|
||
)
|
||
pending = [x for x in items if x["kind"] == "pending_node"]
|
||
self.assertEqual([p["nodeKey"] for p in pending], ["a2", "a3", "e1"])
|
||
self.assertEqual([p["title"] for p in pending], ["二级审批", "三级审批", "结束"])
|
||
self.assertEqual(pending[-1]["taskType"], "end")
|
||
|
||
def test_or_sign_same_node_merged_into_one_card(self):
|
||
"""或签同节点三人只投影一张进度卡片,people 含各人状态。"""
|
||
from app.services.oa_engine import build_progress_items
|
||
|
||
root = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"name": "部门审批",
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
}
|
||
ts = datetime(2026, 7, 17, 10, 0, 0)
|
||
ts2 = datetime(2026, 7, 17, 10, 5, 0)
|
||
|
||
def mk(tid, uid, status, comment=None, update=None):
|
||
return MagicMock(
|
||
id=tid,
|
||
node_key="a1",
|
||
node_name="部门审批",
|
||
task_type="approval",
|
||
status=status,
|
||
assignee_user_id=uid,
|
||
order_num=0,
|
||
round_no=1,
|
||
create_time=ts,
|
||
update_time=update or ts,
|
||
comment_text=comment,
|
||
resolve_source="users",
|
||
sign_mode="or",
|
||
)
|
||
|
||
tasks = [
|
||
mk("t1", "u1", "approved", "同意", ts2),
|
||
mk("t2", "u2", "done"),
|
||
mk("t3", "u3", "done"),
|
||
]
|
||
inst = MagicMock(
|
||
status="completed",
|
||
initiator_user_id="u0",
|
||
current_round=1,
|
||
create_time=ts,
|
||
update_time=ts2,
|
||
)
|
||
items = build_progress_items(
|
||
inst=inst,
|
||
flow_root=root,
|
||
tasks=tasks,
|
||
name_map={"u0": "发起人", "u1": "甲", "u2": "乙", "u3": "丙"},
|
||
events=[],
|
||
)
|
||
task_items = [x for x in items if x["kind"] == "task"]
|
||
self.assertEqual(len(task_items), 1)
|
||
card = task_items[0]
|
||
self.assertEqual(card["nodeKey"], "a1")
|
||
self.assertEqual(card["status"], "approved")
|
||
self.assertEqual(card["signMode"], "or")
|
||
self.assertEqual(len(card["people"]), 3)
|
||
self.assertEqual(
|
||
[(p["nickName"], p["status"]) for p in card["people"]],
|
||
[("甲", "approved"), ("乙", "done"), ("丙", "done")],
|
||
)
|
||
self.assertIn("甲:同意", card["commentText"])
|
||
|
||
def test_terminal_status_excludes_pending_nodes(self):
|
||
from app.services.oa_engine import build_progress_items
|
||
|
||
root = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"name": "审批人",
|
||
"child": {
|
||
"id": "a2",
|
||
"type": "approval",
|
||
"name": "总经理",
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
},
|
||
}
|
||
ts = datetime(2026, 7, 15, 14, 5, 7)
|
||
t_approval = MagicMock(
|
||
id="tA",
|
||
node_key="a1",
|
||
node_name="审批人",
|
||
task_type="approval",
|
||
status="rejected",
|
||
assignee_user_id="u2",
|
||
order_num=0,
|
||
round_no=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
comment_text="不同意",
|
||
resolve_source="users",
|
||
)
|
||
for status in ("rejected", "completed", "cancelled", "returned"):
|
||
inst = MagicMock(
|
||
status=status,
|
||
initiator_user_id="u0",
|
||
current_round=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
)
|
||
items = build_progress_items(
|
||
inst=inst,
|
||
flow_root=root,
|
||
tasks=[t_approval],
|
||
name_map={"u0": "发起人甲", "u2": "审批乙"},
|
||
events=[],
|
||
)
|
||
pending = [x for x in items if x["kind"] == "pending_node"]
|
||
self.assertEqual(pending, [], f"status={status} should not have pending_node")
|
||
terminal = [x for x in items if x["kind"] == "terminal"]
|
||
# 打回仅展示审批人节点「已打回」,不追加独立终态行
|
||
expect_n = 0 if status == "returned" else 1
|
||
self.assertEqual(
|
||
len(terminal), expect_n, f"status={status} terminal count"
|
||
)
|
||
|
||
def test_progress_returned_has_no_kind_return_node(self):
|
||
from app.services.oa_engine import build_progress_items
|
||
|
||
root = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"name": "审批人",
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
}
|
||
ts = datetime(2026, 7, 15, 14, 58, 8)
|
||
t_approval = MagicMock(
|
||
id="tA",
|
||
node_key="a1",
|
||
node_name="审批人",
|
||
task_type="approval",
|
||
status="returned",
|
||
assignee_user_id="u2",
|
||
order_num=1,
|
||
round_no=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
comment_text="打回",
|
||
resolve_source="users",
|
||
)
|
||
ret_ev = MagicMock(
|
||
event_type="return",
|
||
round_no=1,
|
||
actor_user_id="u2",
|
||
create_time=ts,
|
||
comment_text="打回",
|
||
node_key="a1",
|
||
task_id="tA",
|
||
)
|
||
inst = MagicMock(
|
||
status="returned",
|
||
initiator_user_id="u0",
|
||
current_round=1,
|
||
create_time=ts,
|
||
update_time=ts,
|
||
)
|
||
items = build_progress_items(
|
||
inst=inst,
|
||
flow_root=root,
|
||
tasks=[t_approval],
|
||
name_map={"u0": "发起人", "u2": "审批人"},
|
||
events=[ret_ev],
|
||
)
|
||
self.assertFalse(any(x["kind"] == "return" for x in items))
|
||
tasks = [x for x in items if x["kind"] == "task"]
|
||
self.assertEqual(len(tasks), 1)
|
||
self.assertEqual(tasks[0]["status"], "returned")
|
||
self.assertEqual(tasks[0]["commentText"], "打回")
|
||
terminals = [x for x in items if x["kind"] == "terminal"]
|
||
self.assertEqual(terminals, [], "打回不追加独立终态「已打回」行")
|
||
|
||
|
||
class TestReturnAndResubmit(unittest.TestCase):
|
||
def setUp(self):
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
|
||
|
||
self.engine = create_engine("sqlite:///:memory:")
|
||
OaApp.__table__.create(self.engine)
|
||
OaAppVersion.__table__.create(self.engine)
|
||
OaInstance.__table__.create(self.engine)
|
||
OaTask.__table__.create(self.engine)
|
||
OaInstanceEvent.__table__.create(self.engine)
|
||
SysUser.__table__.create(self.engine)
|
||
Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
||
self.db = Session()
|
||
self.OaInstance = OaInstance
|
||
self.OaTask = OaTask
|
||
self.OaInstanceEvent = OaInstanceEvent
|
||
|
||
flow = {
|
||
"id": "s",
|
||
"type": "start",
|
||
"child": {
|
||
"id": "a1",
|
||
"type": "approval",
|
||
"name": "审批人",
|
||
"approverConfig": {
|
||
"assigneeType": "users",
|
||
"userIds": ["u1"],
|
||
"signMode": "or",
|
||
},
|
||
"child": {"id": "e1", "type": "end"},
|
||
},
|
||
}
|
||
now = datetime.now()
|
||
self.db.add(
|
||
OaAppVersion(
|
||
id="v1",
|
||
app_id="app1",
|
||
version_no=1,
|
||
status="published",
|
||
is_latest_published="1",
|
||
form_schema_json="{}",
|
||
flow_tree_json=json.dumps(flow),
|
||
advanced_settings_json=json.dumps({"allowReturn": True}),
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaInstance(
|
||
id="i1",
|
||
app_id="app1",
|
||
app_version_id="v1",
|
||
title="打回测",
|
||
initiator_user_id="u0",
|
||
initiator_dept_id="d1",
|
||
form_data_json="{}",
|
||
status="running",
|
||
current_round=1,
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaTask(
|
||
id="t1",
|
||
instance_id="i1",
|
||
node_key="a1",
|
||
node_name="审批人",
|
||
task_type="approval",
|
||
assignee_user_id="u1",
|
||
status="pending",
|
||
sign_mode="or",
|
||
round_no=1,
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.commit()
|
||
|
||
def tearDown(self):
|
||
self.db.close()
|
||
self.engine.dispose()
|
||
|
||
def test_return_sets_returned_and_event(self):
|
||
from app.services.oa_engine import return_task
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "returned"},
|
||
):
|
||
return_task(
|
||
self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
|
||
)
|
||
inst = self.db.query(self.OaInstance).filter_by(id="i1").first()
|
||
task = self.db.query(self.OaTask).filter_by(id="t1").first()
|
||
ev = (
|
||
self.db.query(self.OaInstanceEvent)
|
||
.filter_by(instance_id="i1", event_type="return")
|
||
.first()
|
||
)
|
||
self.assertEqual(inst.status, "returned")
|
||
self.assertEqual(task.status, "returned")
|
||
self.assertIsNotNone(ev)
|
||
self.assertEqual(ev.comment_text, "请补充材料")
|
||
|
||
def test_resubmit_raises_deprecated(self):
|
||
from app.services.oa_engine import OaEngineError, resubmit_instance, return_task
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "returned"},
|
||
):
|
||
return_task(
|
||
self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
|
||
)
|
||
|
||
with self.assertRaises(OaEngineError) as ctx:
|
||
resubmit_instance(
|
||
self.db,
|
||
"i1",
|
||
tenant_no="zkxw",
|
||
user_id="u0",
|
||
form_data={"x": 1},
|
||
)
|
||
self.assertIn("废弃", str(ctx.exception))
|
||
|
||
def test_done_list_includes_returned(self):
|
||
from app.services.oa_engine import list_tasks, return_task
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "returned"},
|
||
):
|
||
return_task(
|
||
self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
|
||
)
|
||
done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
|
||
self.assertEqual(len(done), 1)
|
||
self.assertEqual(done[0]["status"], "returned")
|
||
|
||
|
||
class TestListTasksEnrichment(unittest.TestCase):
|
||
"""list_tasks 返回整单此刻进度摘要字段。"""
|
||
|
||
def setUp(self):
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
from app.models.models import OaApp, OaAppVersion, OaInstance, OaTask, SysUser
|
||
|
||
self.engine = create_engine("sqlite:///:memory:")
|
||
OaApp.__table__.create(self.engine)
|
||
OaAppVersion.__table__.create(self.engine)
|
||
OaInstance.__table__.create(self.engine)
|
||
OaTask.__table__.create(self.engine)
|
||
SysUser.__table__.create(self.engine)
|
||
Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
||
self.db = Session()
|
||
self.OaTask = OaTask
|
||
now = datetime(2026, 7, 15, 10, 0, 0)
|
||
self.db.add(
|
||
OaApp(
|
||
id="app1",
|
||
app_name="请假审批",
|
||
initiator_scope="all",
|
||
admin_scope="all_oa_admin",
|
||
status="1",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaAppVersion(
|
||
id="v1",
|
||
app_id="app1",
|
||
version_no=1,
|
||
status="published",
|
||
is_latest_published="1",
|
||
form_schema_json="{}",
|
||
flow_tree_json="{}",
|
||
advanced_settings_json="{}",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
SysUser(
|
||
id="u0",
|
||
user_no="initiator",
|
||
nick_name="发起人甲",
|
||
password="x",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
SysUser(
|
||
id="u1",
|
||
user_no="approver1",
|
||
nick_name="审批人乙",
|
||
password="x",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
SysUser(
|
||
id="u2",
|
||
user_no="approver2",
|
||
nick_name="审批人丙",
|
||
password="x",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaInstance(
|
||
id="i1",
|
||
app_id="app1",
|
||
app_version_id="v1",
|
||
title="请假-忽略",
|
||
initiator_user_id="u0",
|
||
initiator_dept_id="d1",
|
||
form_data_json="{}",
|
||
status="running",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaTask(
|
||
id="t-done",
|
||
instance_id="i1",
|
||
node_key="a1",
|
||
node_name="一级审批",
|
||
task_type="approval",
|
||
assignee_user_id="u1",
|
||
status="approved",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaTask(
|
||
id="t-pending",
|
||
instance_id="i1",
|
||
node_key="a2",
|
||
node_name="二级审批",
|
||
task_type="approval",
|
||
assignee_user_id="u2",
|
||
status="pending",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.commit()
|
||
|
||
def tearDown(self):
|
||
self.db.close()
|
||
self.engine.dispose()
|
||
|
||
def test_done_list_shows_instance_current_progress(self):
|
||
from app.services.oa_engine import list_tasks
|
||
|
||
done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
|
||
self.assertEqual(len(done), 1)
|
||
row = done[0]
|
||
self.assertEqual(row["appName"], "请假审批")
|
||
self.assertEqual(row["initiatorUserName"], "发起人甲")
|
||
self.assertEqual(row["applyTime"], "2026-07-15 10:00:00")
|
||
self.assertEqual(row["instanceStatus"], "running")
|
||
self.assertEqual(row["currentNodeName"], "二级审批")
|
||
self.assertEqual(row["currentApproverNames"], "审批人丙")
|
||
self.assertEqual(row["status"], "approved")
|
||
|
||
def test_mine_list_enrichment(self):
|
||
from app.services.oa_engine import list_tasks
|
||
|
||
mine = list_tasks(self.db, tenant_no="zkxw", user_id="u0", kind="mine")
|
||
self.assertEqual(len(mine), 1)
|
||
row = mine[0]
|
||
self.assertEqual(row["appName"], "请假审批")
|
||
self.assertEqual(row["instanceStatus"], "running")
|
||
self.assertEqual(row["currentNodeName"], "二级审批")
|
||
self.assertEqual(row["currentApproverNames"], "审批人丙")
|
||
|
||
def test_pending_list_enrichment(self):
|
||
from app.services.oa_engine import list_tasks
|
||
|
||
pending = list_tasks(self.db, tenant_no="zkxw", user_id="u2", kind="pending")
|
||
self.assertEqual(len(pending), 1)
|
||
row = pending[0]
|
||
self.assertEqual(row["appName"], "请假审批")
|
||
self.assertEqual(row["currentNodeName"], "二级审批")
|
||
|
||
def test_terminal_instance_labels(self):
|
||
from app.services.oa_engine import list_tasks
|
||
from app.models.models import OaInstance
|
||
|
||
cases = [
|
||
("completed", "已完成"),
|
||
("rejected", "已拒绝"),
|
||
("returned", "已打回"),
|
||
("cancelled", "已撤销"),
|
||
]
|
||
inst = self.db.query(OaInstance).filter_by(id="i1").first()
|
||
pending = self.db.query(self.OaTask).filter_by(id="t-pending").first()
|
||
for status, expected_label in cases:
|
||
with self.subTest(status=status):
|
||
inst.status = status
|
||
pending.status = "cancelled"
|
||
self.db.commit()
|
||
|
||
mine = list_tasks(self.db, tenant_no="zkxw", user_id="u0", kind="mine")
|
||
row = mine[0]
|
||
self.assertEqual(row["instanceStatus"], status)
|
||
self.assertEqual(row["currentNodeName"], expected_label)
|
||
self.assertEqual(row["currentApproverNames"], "-")
|
||
|
||
inst.status = "running"
|
||
pending.status = "pending"
|
||
self.db.commit()
|
||
|
||
|
||
class TestTransferTask(unittest.TestCase):
|
||
def setUp(self):
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
|
||
|
||
self.engine = create_engine("sqlite:///:memory:")
|
||
OaApp.__table__.create(self.engine)
|
||
OaAppVersion.__table__.create(self.engine)
|
||
OaInstance.__table__.create(self.engine)
|
||
OaTask.__table__.create(self.engine)
|
||
OaInstanceEvent.__table__.create(self.engine)
|
||
SysUser.__table__.create(self.engine)
|
||
Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
||
self.db = Session()
|
||
self.OaTask = OaTask
|
||
self.OaInstance = OaInstance
|
||
self.OaAppVersion = OaAppVersion
|
||
self.SysUser = SysUser
|
||
now = datetime.now()
|
||
self.db.add(
|
||
OaApp(
|
||
id="app1",
|
||
group_id="",
|
||
app_name="测试应用",
|
||
icon_url="",
|
||
description="",
|
||
initiator_scope="all",
|
||
initiator_user_ids="[]",
|
||
admin_scope="all",
|
||
admin_user_ids="[]",
|
||
status="published",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaAppVersion(
|
||
id="v1",
|
||
app_id="app1",
|
||
version_no=1,
|
||
status="published",
|
||
is_latest_published="1",
|
||
form_schema_json="{}",
|
||
flow_tree_json="{}",
|
||
advanced_settings_json="{}",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaInstance(
|
||
id="i1",
|
||
app_id="app1",
|
||
app_version_id="v1",
|
||
title="t",
|
||
status="running",
|
||
initiator_user_id="u0",
|
||
initiator_dept_id="d1",
|
||
form_data_json="{}",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
current_round=1,
|
||
)
|
||
)
|
||
for uid, name in (("u1", "李四"), ("u2", "赵六"), ("u3", "钱七")):
|
||
self.db.add(
|
||
SysUser(
|
||
id=uid,
|
||
user_no=uid,
|
||
nick_name=name,
|
||
email="",
|
||
phone_no="",
|
||
password="",
|
||
status="1",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaTask(
|
||
id="t1",
|
||
instance_id="i1",
|
||
node_key="a1",
|
||
node_name="审批人",
|
||
task_type="approval",
|
||
assignee_user_id="u1",
|
||
status="pending",
|
||
resolve_source="role",
|
||
sign_mode="and",
|
||
round_no=1,
|
||
tenant_no="zkxw",
|
||
order_num=1,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaTask(
|
||
id="t2",
|
||
instance_id="i1",
|
||
node_key="a1",
|
||
node_name="审批人",
|
||
task_type="approval",
|
||
assignee_user_id="u3",
|
||
status="pending",
|
||
resolve_source="role",
|
||
sign_mode="and",
|
||
round_no=1,
|
||
tenant_no="zkxw",
|
||
order_num=2,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.commit()
|
||
|
||
def tearDown(self):
|
||
self.db.close()
|
||
self.engine.dispose()
|
||
|
||
def test_transfer_rejects_self(self):
|
||
from app.services.oa_engine import OaEngineError, transfer_task
|
||
|
||
with self.assertRaises(OaEngineError) as ctx:
|
||
transfer_task(
|
||
self.db, "t1", tenant_no="zkxw", user_id="u1", target_user_id="u1"
|
||
)
|
||
self.assertIn("自己", str(ctx.exception))
|
||
|
||
def test_transfer_rejects_existing_pending(self):
|
||
from app.services.oa_engine import OaEngineError, transfer_task
|
||
|
||
with self.assertRaises(OaEngineError) as ctx:
|
||
transfer_task(
|
||
self.db, "t1", tenant_no="zkxw", user_id="u1", target_user_id="u3"
|
||
)
|
||
self.assertIn("待办", str(ctx.exception))
|
||
|
||
def test_transfer_rejects_non_assignee(self):
|
||
from app.services.oa_engine import OaEngineError, transfer_task
|
||
|
||
with self.assertRaises(OaEngineError) as ctx:
|
||
transfer_task(
|
||
self.db, "t1", tenant_no="zkxw", user_id="u2", target_user_id="u2"
|
||
)
|
||
self.assertIn("无权", str(ctx.exception))
|
||
|
||
def test_transfer_success_and_keeps_co_approver(self):
|
||
from app.services.oa_engine import list_tasks, transfer_task
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "running"},
|
||
):
|
||
transfer_task(
|
||
self.db,
|
||
"t1",
|
||
tenant_no="zkxw",
|
||
user_id="u1",
|
||
target_user_id="u2",
|
||
comment="请协助处理",
|
||
)
|
||
|
||
t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
|
||
t2 = self.db.query(self.OaTask).filter(self.OaTask.id == "t2").first()
|
||
new_task = (
|
||
self.db.query(self.OaTask)
|
||
.filter(
|
||
self.OaTask.instance_id == "i1",
|
||
self.OaTask.assignee_user_id == "u2",
|
||
self.OaTask.status == "pending",
|
||
)
|
||
.first()
|
||
)
|
||
self.assertEqual(t1.status, "transferred")
|
||
self.assertEqual(t1.comment_text, "请协助处理")
|
||
self.assertEqual(t2.status, "pending")
|
||
self.assertIsNotNone(new_task)
|
||
self.assertEqual(new_task.resolve_source, "transfer")
|
||
self.assertEqual(new_task.sign_mode, "and")
|
||
self.assertEqual(new_task.node_key, "a1")
|
||
|
||
done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
|
||
self.assertEqual(len(done), 1)
|
||
self.assertEqual(done[0]["status"], "transferred")
|
||
|
||
def test_transfer_comment_optional(self):
|
||
from app.services.oa_engine import transfer_task
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "running"},
|
||
):
|
||
transfer_task(
|
||
self.db,
|
||
"t1",
|
||
tenant_no="zkxw",
|
||
user_id="u1",
|
||
target_user_id="u2",
|
||
)
|
||
|
||
t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
|
||
self.assertEqual(t1.status, "transferred")
|
||
self.assertIsNone(t1.comment_text)
|
||
|
||
def test_transfer_chain(self):
|
||
from app.services.oa_engine import transfer_task
|
||
|
||
now = datetime.now()
|
||
self.db.add(
|
||
self.SysUser(
|
||
id="u4",
|
||
user_no="u4",
|
||
nick_name="孙八",
|
||
email="",
|
||
phone_no="",
|
||
password="",
|
||
status="1",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.commit()
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "running"},
|
||
):
|
||
transfer_task(
|
||
self.db,
|
||
"t1",
|
||
tenant_no="zkxw",
|
||
user_id="u1",
|
||
target_user_id="u2",
|
||
)
|
||
|
||
u2_task = (
|
||
self.db.query(self.OaTask)
|
||
.filter(
|
||
self.OaTask.instance_id == "i1",
|
||
self.OaTask.assignee_user_id == "u2",
|
||
self.OaTask.status == "pending",
|
||
)
|
||
.first()
|
||
)
|
||
self.assertIsNotNone(u2_task)
|
||
|
||
with patch(
|
||
"app.services.oa_engine.get_instance_detail",
|
||
return_value={"id": "i1", "status": "running"},
|
||
):
|
||
transfer_task(
|
||
self.db,
|
||
u2_task.id,
|
||
tenant_no="zkxw",
|
||
user_id="u2",
|
||
target_user_id="u4",
|
||
)
|
||
|
||
u2_task = self.db.query(self.OaTask).filter(self.OaTask.id == u2_task.id).first()
|
||
u4_task = (
|
||
self.db.query(self.OaTask)
|
||
.filter(
|
||
self.OaTask.instance_id == "i1",
|
||
self.OaTask.assignee_user_id == "u4",
|
||
self.OaTask.status == "pending",
|
||
)
|
||
.first()
|
||
)
|
||
self.assertEqual(u2_task.status, "transferred")
|
||
self.assertIsNotNone(u4_task)
|
||
self.assertEqual(u4_task.resolve_source, "transfer")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|
||
|