test_oa_assignee.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. """OA 审批人解析单元测试"""
  2. import unittest
  3. from unittest.mock import MagicMock, patch
  4. from app.services.oa_assignee import (
  5. AssigneeResolveError,
  6. normalize_cc_assignee_types,
  7. resolve_assignees,
  8. resolve_cc_assignees,
  9. )
  10. class TestOaAssignee(unittest.TestCase):
  11. def test_users_resolves_active_ids(self):
  12. db = MagicMock()
  13. with patch(
  14. "app.services.oa_assignee._active_user_id",
  15. side_effect=lambda _db, uid, _tn: uid if uid else None,
  16. ):
  17. users, meta = resolve_assignees(
  18. db,
  19. tenant_no="zkxw",
  20. assignee_config={"assigneeType": "users", "userIds": ["u1", "u1", ""]},
  21. initiator_user_id="initiator",
  22. )
  23. self.assertEqual(users, ["u1"])
  24. self.assertEqual(meta.get("resolve_source"), "users")
  25. def test_dept_leader_resolves_by_initiator_dept(self):
  26. db = MagicMock()
  27. dept = MagicMock(leader_user_id="leaderA")
  28. db.query.return_value.filter.return_value.first.return_value = dept
  29. with patch(
  30. "app.services.oa_assignee._active_user_id",
  31. side_effect=lambda _db, uid, _tn: uid if uid else None,
  32. ):
  33. users, meta = resolve_assignees(
  34. db,
  35. tenant_no="zkxw",
  36. assignee_config={"assigneeType": "dept_leader"},
  37. initiator_user_id="zhang",
  38. initiator_dept_id="deptA",
  39. )
  40. self.assertEqual(users, ["leaderA"])
  41. self.assertEqual(meta.get("resolve_source"), "dept_leader")
  42. def test_direct_supervisor_fallback_to_dept_leader(self):
  43. db = MagicMock()
  44. initiator = MagicMock(supervisor_user_id=None, dept_id="deptA")
  45. dept = MagicMock(leader_user_id="leaderA")
  46. db.query.return_value.filter.return_value.first.side_effect = [initiator, dept]
  47. with patch(
  48. "app.services.oa_assignee._active_user_id",
  49. side_effect=lambda _db, uid, _tn: uid if uid else None,
  50. ):
  51. users, meta = resolve_assignees(
  52. db,
  53. tenant_no="zkxw",
  54. assignee_config={"assigneeType": "direct_supervisor"},
  55. initiator_user_id="no_supervisor",
  56. initiator_dept_id="deptA",
  57. advanced={"supervisorFallbackToDeptLeader": True},
  58. )
  59. self.assertEqual(users, ["leaderA"])
  60. self.assertEqual(meta.get("resolve_source"), "supervisor_fallback_dept_leader")
  61. def test_direct_supervisor_allows_self(self):
  62. """部门经理等场景:直属主管可配置为本人,解析应成功。"""
  63. db = MagicMock()
  64. initiator = MagicMock(supervisor_user_id="manager_self", dept_id="deptA")
  65. db.query.return_value.filter.return_value.first.return_value = initiator
  66. with patch(
  67. "app.services.oa_assignee._active_user_id",
  68. side_effect=lambda _db, uid, _tn: uid if uid else None,
  69. ):
  70. users, meta = resolve_assignees(
  71. db,
  72. tenant_no="zkxw",
  73. assignee_config={"assigneeType": "direct_supervisor"},
  74. initiator_user_id="manager_self",
  75. initiator_dept_id="deptA",
  76. )
  77. self.assertEqual(users, ["manager_self"])
  78. self.assertEqual(meta.get("resolve_source"), "direct_supervisor")
  79. def test_direct_supervisor_no_fallback_raises(self):
  80. db = MagicMock()
  81. initiator = MagicMock(supervisor_user_id=None, dept_id="deptA")
  82. db.query.return_value.filter.return_value.first.return_value = initiator
  83. with patch(
  84. "app.services.oa_assignee._active_user_id",
  85. return_value=None,
  86. ):
  87. with self.assertRaises(AssigneeResolveError):
  88. resolve_assignees(
  89. db,
  90. tenant_no="zkxw",
  91. assignee_config={"assigneeType": "direct_supervisor"},
  92. initiator_user_id="no_supervisor",
  93. initiator_dept_id="deptA",
  94. advanced={"supervisorFallbackToDeptLeader": False},
  95. )
  96. def test_posts_resolve(self):
  97. db = MagicMock()
  98. with patch(
  99. "app.services.oa_assignee.user_crud.list_user_ids_bound_to_posts",
  100. return_value=["gm_user", "inactive"],
  101. ), patch(
  102. "app.services.oa_assignee._active_user_id",
  103. side_effect=lambda _db, uid, _tn: uid if uid == "gm_user" else None,
  104. ):
  105. users, meta = resolve_assignees(
  106. db,
  107. tenant_no="zkxw",
  108. assignee_config={"assigneeType": "posts", "postIds": ["post_gm"]},
  109. initiator_user_id="zhang",
  110. )
  111. self.assertEqual(users, ["gm_user"])
  112. self.assertEqual(meta.get("resolve_source"), "posts")
  113. def test_never_reads_sys_role(self):
  114. db = MagicMock()
  115. with patch(
  116. "app.services.oa_assignee.user_crud.list_user_ids_bound_to_posts",
  117. return_value=["post_only"],
  118. ) as mock_list, patch(
  119. "app.services.oa_assignee._active_user_id",
  120. side_effect=lambda _db, uid, _tn: uid,
  121. ):
  122. users, _meta = resolve_assignees(
  123. db,
  124. tenant_no="zkxw",
  125. assignee_config={"assigneeType": "posts", "postIds": ["p1"]},
  126. initiator_user_id="zhang",
  127. )
  128. self.assertEqual(users, ["post_only"])
  129. mock_list.assert_called_once()
  130. for call in db.query.call_args_list:
  131. args = call[0] if call[0] else ()
  132. for a in args:
  133. name = getattr(a, "__name__", None) or getattr(a, "__tablename__", None) or str(a)
  134. self.assertNotIn("SysRole", str(name))
  135. self.assertNotIn("sys_role", str(name))
  136. def test_initiator_select_empty_raises(self):
  137. db = MagicMock()
  138. with self.assertRaises(AssigneeResolveError):
  139. resolve_assignees(
  140. db,
  141. tenant_no="zkxw",
  142. assignee_config={"assigneeType": "initiator_select"},
  143. initiator_user_id="zhang",
  144. initiator_select_user_ids=[],
  145. )
  146. def test_normalize_prefers_assignee_types(self):
  147. self.assertEqual(
  148. normalize_cc_assignee_types(
  149. {"assigneeTypes": ["users", "dept_leader", "users", "bad"], "assigneeType": "posts"}
  150. ),
  151. ["users", "dept_leader"],
  152. )
  153. def test_normalize_fallback_single(self):
  154. self.assertEqual(
  155. normalize_cc_assignee_types({"assigneeType": "initiator_select"}),
  156. ["initiator_select"],
  157. )
  158. self.assertEqual(normalize_cc_assignee_types({}), [])
  159. def test_resolve_cc_union_dedupe(self):
  160. db = MagicMock()
  161. with patch(
  162. "app.services.oa_assignee.resolve_assignees",
  163. side_effect=[
  164. (["u1", "u2"], {"resolve_source": "users"}),
  165. (["u2", "u3"], {"resolve_source": "dept_leader"}),
  166. ],
  167. ) as mock_resolve:
  168. users, meta = resolve_cc_assignees(
  169. db,
  170. tenant_no="zkxw",
  171. assignee_config={
  172. "assigneeTypes": ["users", "dept_leader"],
  173. "userIds": ["u1", "u2"],
  174. },
  175. initiator_user_id="zhang",
  176. initiator_dept_id="d1",
  177. )
  178. self.assertEqual(users, ["u1", "u2", "u3"])
  179. self.assertEqual(meta.get("resolve_source"), "multi")
  180. self.assertEqual(meta.get("sources"), ["users", "dept_leader"])
  181. self.assertEqual(mock_resolve.call_count, 2)
  182. def test_resolve_cc_skips_failed_strategy(self):
  183. db = MagicMock()
  184. with patch(
  185. "app.services.oa_assignee.resolve_assignees",
  186. side_effect=[
  187. AssigneeResolveError("部门负责人未配置"),
  188. (["u1"], {"resolve_source": "users"}),
  189. ],
  190. ):
  191. users, meta = resolve_cc_assignees(
  192. db,
  193. tenant_no="zkxw",
  194. assignee_config={
  195. "assigneeTypes": ["dept_leader", "users"],
  196. "userIds": ["u1"],
  197. },
  198. initiator_user_id="zhang",
  199. initiator_dept_id="d1",
  200. )
  201. self.assertEqual(users, ["u1"])
  202. self.assertEqual(meta.get("resolve_source"), "users")
  203. self.assertEqual(len(meta.get("skipped") or []), 1)
  204. def test_resolve_cc_all_fail_returns_empty(self):
  205. db = MagicMock()
  206. with patch(
  207. "app.services.oa_assignee.resolve_assignees",
  208. side_effect=AssigneeResolveError("fail"),
  209. ):
  210. users, meta = resolve_cc_assignees(
  211. db,
  212. tenant_no="zkxw",
  213. assignee_config={"assigneeTypes": ["dept_leader"]},
  214. initiator_user_id="zhang",
  215. initiator_dept_id="d1",
  216. )
  217. self.assertEqual(users, [])
  218. self.assertTrue(meta.get("skipped"))
  219. def test_resolve_cc_legacy_single_type(self):
  220. db = MagicMock()
  221. with patch(
  222. "app.services.oa_assignee.resolve_assignees",
  223. return_value=(["u1"], {"resolve_source": "users"}),
  224. ) as mock_resolve:
  225. users, meta = resolve_cc_assignees(
  226. db,
  227. tenant_no="zkxw",
  228. assignee_config={"assigneeType": "users", "userIds": ["u1"]},
  229. initiator_user_id="zhang",
  230. )
  231. self.assertEqual(users, ["u1"])
  232. self.assertEqual(meta.get("resolve_source"), "users")
  233. single_cfg = mock_resolve.call_args.kwargs["assignee_config"]
  234. self.assertEqual(single_cfg.get("assigneeType"), "users")
  235. if __name__ == "__main__":
  236. unittest.main()