test_oa_publish.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. """OA 发布校验单元测试"""
  2. import unittest
  3. from unittest.mock import MagicMock, patch
  4. from app.services.oa_publish import (
  5. count_fillable_fields,
  6. validate_condition_rules_against_form,
  7. validate_control_types,
  8. validate_draft_for_publish,
  9. validate_flow_tree,
  10. )
  11. def _linear_ok_flow():
  12. return {
  13. "id": "s1",
  14. "type": "start",
  15. "name": "发起人",
  16. "child": {
  17. "id": "a1",
  18. "type": "approval",
  19. "name": "部门负责人",
  20. "approverConfig": {"assigneeType": "dept_leader"},
  21. "child": {"id": "e1", "type": "end", "name": "结束"},
  22. },
  23. }
  24. def _condition_flow(rules=None, when=None, is_default=False):
  25. return {
  26. "type": "start",
  27. "name": "发起人",
  28. "child": {
  29. "type": "condition",
  30. "name": "条件",
  31. "conditionGroups": [
  32. {
  33. "name": "分支1",
  34. "rules": rules if rules is not None else [],
  35. "when": when,
  36. "isDefault": False,
  37. "child": {
  38. "type": "approval",
  39. "name": "审批A",
  40. "approverConfig": {"assigneeType": "dept_leader"},
  41. "child": {"type": "end", "name": "结束"},
  42. },
  43. },
  44. {
  45. "name": "默认",
  46. "rules": [],
  47. "isDefault": True,
  48. "child": {
  49. "type": "approval",
  50. "name": "审批B",
  51. "approverConfig": {"assigneeType": "dept_leader"},
  52. "child": {"type": "end", "name": "结束"},
  53. },
  54. },
  55. ],
  56. },
  57. }
  58. class TestOaPublishValidate(unittest.TestCase):
  59. def test_count_fillable_fields(self):
  60. schema = {
  61. "rule": [
  62. {"type": "html", "props": {"innerHTML": "说明"}},
  63. {"type": "input", "field": "title", "title": "标题"},
  64. {
  65. "type": "fcRow",
  66. "children": [
  67. {"type": "inputNumber", "field": "days", "title": "天数"},
  68. ],
  69. },
  70. ]
  71. }
  72. self.assertEqual(count_fillable_fields(schema), 2)
  73. self.assertEqual(count_fillable_fields({"rule": []}), 0)
  74. def test_flow_ok(self):
  75. self.assertEqual(validate_flow_tree(_linear_ok_flow()), [])
  76. def test_flow_missing_end(self):
  77. tree = {
  78. "type": "start",
  79. "name": "发起人",
  80. "child": {
  81. "type": "approval",
  82. "name": "审批",
  83. "approverConfig": {"assigneeType": "users", "userIds": ["u1"]},
  84. },
  85. }
  86. errs = validate_flow_tree(tree)
  87. self.assertTrue(any("结束" in e or "后续" in e for e in errs))
  88. def test_approval_users_empty_rejected(self):
  89. tree = {
  90. "type": "start",
  91. "child": {
  92. "type": "approval",
  93. "name": "审批",
  94. "approverConfig": {"assigneeType": "users", "userIds": []},
  95. "child": {"type": "end"},
  96. },
  97. }
  98. errs = validate_flow_tree(tree)
  99. self.assertTrue(any("指定成员" in e for e in errs))
  100. def test_cc_multi_types_ok(self):
  101. flow = {
  102. "type": "start",
  103. "child": {
  104. "type": "cc",
  105. "name": "抄送",
  106. "approverConfig": {
  107. "assigneeTypes": ["users", "dept_leader"],
  108. "userIds": ["u1"],
  109. },
  110. "child": {"type": "end"},
  111. },
  112. }
  113. self.assertEqual(validate_flow_tree(flow), [])
  114. def test_cc_multi_users_empty_fails(self):
  115. flow = {
  116. "type": "start",
  117. "child": {
  118. "type": "cc",
  119. "name": "抄送",
  120. "approverConfig": {
  121. "assigneeTypes": ["users", "initiator_select"],
  122. "userIds": [],
  123. },
  124. "child": {"type": "end"},
  125. },
  126. }
  127. errs = validate_flow_tree(flow)
  128. self.assertTrue(any("指定成员" in e for e in errs))
  129. def test_cc_no_types_fails(self):
  130. flow = {
  131. "type": "start",
  132. "child": {
  133. "type": "cc",
  134. "name": "抄送",
  135. "approverConfig": {},
  136. "child": {"type": "end"},
  137. },
  138. }
  139. errs = validate_flow_tree(flow)
  140. self.assertTrue(len(errs) >= 1)
  141. def test_approval_ignores_assignee_types_array(self):
  142. flow = {
  143. "type": "start",
  144. "child": {
  145. "type": "approval",
  146. "name": "审批",
  147. "approverConfig": {"assigneeTypes": ["dept_leader"]},
  148. "child": {"type": "end"},
  149. },
  150. }
  151. errs = validate_flow_tree(flow)
  152. self.assertTrue(len(errs) >= 1)
  153. def test_cc_legacy_single_assignee_type_ok(self):
  154. flow = {
  155. "type": "start",
  156. "child": {
  157. "type": "cc",
  158. "name": "抄送",
  159. "approverConfig": {"assigneeType": "initiator_select"},
  160. "child": {"type": "end"},
  161. },
  162. }
  163. self.assertEqual(validate_flow_tree(flow), [])
  164. def test_condition_branch_incomplete(self):
  165. tree = {
  166. "type": "start",
  167. "child": {
  168. "type": "condition",
  169. "name": "条件",
  170. "conditionGroups": [{"name": "大于3天", "rules": [], "child": None}],
  171. },
  172. }
  173. errs = validate_flow_tree(tree)
  174. self.assertTrue(any("下游" in e or "结束" in e for e in errs))
  175. def test_draft_requires_fillable_and_assignee(self):
  176. db = MagicMock()
  177. with patch(
  178. "app.services.oa_publish.validate_posts_referenced",
  179. return_value=[],
  180. ):
  181. errs = validate_draft_for_publish(
  182. db,
  183. tenant_no="zkxw",
  184. form_schema={"rule": [{"type": "html"}]},
  185. flow_tree={
  186. "type": "start",
  187. "child": {
  188. "type": "approval",
  189. "approverConfig": {"assigneeType": "users", "userIds": []},
  190. "child": {"type": "end"},
  191. },
  192. },
  193. )
  194. self.assertTrue(any("可填字段" in e for e in errs))
  195. self.assertTrue(any("指定成员" in e for e in errs))
  196. def test_valid_draft_passes_form_and_flow(self):
  197. db = MagicMock()
  198. with patch(
  199. "app.services.oa_publish.validate_posts_referenced",
  200. return_value=[],
  201. ):
  202. errs = validate_draft_for_publish(
  203. db,
  204. tenant_no="zkxw",
  205. form_schema={"rule": [{"type": "input", "field": "reason"}]},
  206. flow_tree=_linear_ok_flow(),
  207. )
  208. self.assertEqual(errs, [])
  209. def test_unknown_control_type_rejected(self):
  210. errs = validate_control_types(
  211. {"rule": [{"type": "aiMagicBox", "field": "x"}, {"type": "input", "field": "y"}]}
  212. )
  213. self.assertTrue(any("aiMagicBox" in e for e in errs))
  214. def test_upload_is_allowed(self):
  215. self.assertEqual(
  216. validate_control_types(
  217. {"rule": [{"type": "upload", "field": "files", "title": "上传"}]}
  218. ),
  219. [],
  220. )
  221. self.assertEqual(
  222. count_fillable_fields(
  223. {"rule": [{"type": "upload", "field": "files", "title": "上传"}]}
  224. ),
  225. 1,
  226. )
  227. class TestConditionPublish(unittest.TestCase):
  228. def test_when_only_rejected(self):
  229. errs = validate_flow_tree(_condition_flow(rules=[], when="{days} > 3"))
  230. self.assertTrue(any("可视化" in e or "重新配置" in e or "rules" in e for e in errs))
  231. def test_rules_empty_non_default_rejected(self):
  232. errs = validate_flow_tree(_condition_flow(rules=[]))
  233. self.assertTrue(any("条件" in e for e in errs))
  234. def test_rules_ok(self):
  235. errs = validate_flow_tree(
  236. _condition_flow(rules=[{"field": "days", "op": ">", "value": 3}])
  237. )
  238. self.assertEqual(errs, [])
  239. def test_invalid_field_reference(self):
  240. db = MagicMock()
  241. with patch(
  242. "app.services.oa_publish.validate_posts_referenced",
  243. return_value=[],
  244. ):
  245. errs = validate_draft_for_publish(
  246. db,
  247. tenant_no="zkxw",
  248. form_schema={
  249. "rule": [
  250. {
  251. "type": "inputNumber",
  252. "field": "days",
  253. "title": "天数",
  254. "useInCondition": True,
  255. "$required": True,
  256. }
  257. ]
  258. },
  259. flow_tree=_condition_flow(
  260. rules=[{"field": "other", "op": ">", "value": 1}]
  261. ),
  262. )
  263. self.assertTrue(any("无效字段" in e or "other" in e for e in errs))
  264. def test_field_not_flagged_rejected(self):
  265. db = MagicMock()
  266. with patch(
  267. "app.services.oa_publish.validate_posts_referenced",
  268. return_value=[],
  269. ):
  270. errs = validate_draft_for_publish(
  271. db,
  272. tenant_no="zkxw",
  273. form_schema={
  274. "rule": [
  275. {
  276. "type": "inputNumber",
  277. "field": "days",
  278. "title": "天数",
  279. "useInCondition": False,
  280. }
  281. ]
  282. },
  283. flow_tree=_condition_flow(
  284. rules=[{"field": "days", "op": ">", "value": 3}]
  285. ),
  286. )
  287. self.assertTrue(any("无效字段" in e or "参与" in e for e in errs))
  288. def test_initiator_condition_field_allowed(self):
  289. errs = validate_condition_rules_against_form(
  290. {"rule": []},
  291. _condition_flow(
  292. rules=[
  293. {
  294. "field": "__initiator__",
  295. "op": "contains",
  296. "value": ["u1"],
  297. }
  298. ]
  299. ),
  300. )
  301. self.assertEqual(errs, [])
  302. def test_initiator_condition_empty_value_rejected(self):
  303. errs = validate_condition_rules_against_form(
  304. {"rule": []},
  305. _condition_flow(
  306. rules=[
  307. {
  308. "field": "__initiator__",
  309. "op": "contains",
  310. "value": [],
  311. }
  312. ]
  313. ),
  314. )
  315. self.assertTrue(any("发起人" in e and ("包含" in e or "用户" in e) for e in errs))
  316. def test_initiator_condition_bad_op_rejected(self):
  317. errs = validate_condition_rules_against_form(
  318. {"rule": []},
  319. _condition_flow(
  320. rules=[
  321. {
  322. "field": "__initiator__",
  323. "op": "==",
  324. "value": ["u1"],
  325. }
  326. ]
  327. ),
  328. )
  329. self.assertTrue(any("发起人" in e and "包含" in e for e in errs))
  330. def test_dept_condition_field_allowed(self):
  331. errs = validate_condition_rules_against_form(
  332. {
  333. "rule": [
  334. {
  335. "type": "dept",
  336. "field": "Fdept",
  337. "title": "部门",
  338. "useInCondition": True,
  339. "$required": True,
  340. "props": {"multiple": True},
  341. }
  342. ]
  343. },
  344. _condition_flow(
  345. rules=[
  346. {"field": "Fdept", "op": "contains", "value": ["d1", "d2"]}
  347. ]
  348. ),
  349. )
  350. self.assertEqual(errs, [])
  351. def test_owner_dept_condition_field_allowed(self):
  352. errs = validate_condition_rules_against_form(
  353. {
  354. "rule": [
  355. {
  356. "type": "ownerDept",
  357. "field": "Fod",
  358. "title": "归属部门",
  359. "useInCondition": True,
  360. "$required": True,
  361. }
  362. ]
  363. },
  364. _condition_flow(
  365. rules=[{"field": "Fod", "op": "contains", "value": ["d1"]}]
  366. ),
  367. )
  368. self.assertEqual(errs, [])
  369. if __name__ == "__main__":
  370. unittest.main()