test_oa_instance_comment.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. """OA 实例评论:附件枚举与引用校验"""
  2. import json
  3. import unittest
  4. from datetime import datetime
  5. from app.services.oa_instance_comment import (
  6. enumerate_form_attachments,
  7. normalize_attachment_refs,
  8. )
  9. class TestEnumerateFormAttachments(unittest.TestCase):
  10. def test_groups_upload_fields(self):
  11. schema = {
  12. "rule": [
  13. {
  14. "type": "upload",
  15. "field": "files",
  16. "title": "附件",
  17. },
  18. {"type": "input", "field": "name", "title": "名称"},
  19. ]
  20. }
  21. form = {
  22. "files": [
  23. {"url": "https://x/a.pdf", "name": "a.pdf"},
  24. {"url": "https://x/b.pdf", "name": "b.pdf"},
  25. ],
  26. "name": "demo",
  27. }
  28. items = enumerate_form_attachments(schema, form)
  29. self.assertEqual(len(items), 1)
  30. self.assertEqual(items[0]["fieldKey"], "files")
  31. self.assertEqual(items[0]["fieldLabel"], "附件")
  32. self.assertEqual(len(items[0]["files"]), 2)
  33. self.assertEqual(items[0]["files"][0]["fileName"], "a.pdf")
  34. def test_skips_empty_upload(self):
  35. schema = {"rule": [{"type": "fcUpload", "field": "f", "title": "F"}]}
  36. self.assertEqual(enumerate_form_attachments(schema, {"f": []}), [])
  37. class TestNormalizeAttachmentRefs(unittest.TestCase):
  38. def test_accepts_valid_ref(self):
  39. schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]}
  40. form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]}
  41. out = normalize_attachment_refs(
  42. schema,
  43. form,
  44. [{"fieldKey": "files", "url": "https://x/a.pdf", "fileName": "wrong"}],
  45. )
  46. self.assertEqual(out[0]["fileName"], "a.pdf")
  47. self.assertEqual(out[0]["fieldLabel"], "附件")
  48. def test_rejects_foreign_url(self):
  49. from app.services.oa_engine import OaEngineError
  50. schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]}
  51. form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]}
  52. with self.assertRaises(OaEngineError):
  53. normalize_attachment_refs(
  54. schema,
  55. form,
  56. [{"fieldKey": "files", "url": "https://evil/x.pdf"}],
  57. )
  58. def test_marks_form_ref_source(self):
  59. schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]}
  60. form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]}
  61. out = normalize_attachment_refs(
  62. schema,
  63. form,
  64. [{"url": "https://x/a.pdf"}],
  65. )
  66. self.assertEqual(out[0]["source"], "form")
  67. def test_accepts_tenant_upload_and_uses_stored_name(self):
  68. from sqlalchemy import create_engine
  69. from sqlalchemy.orm import sessionmaker
  70. from app.models.models import BizAttachment
  71. engine = create_engine("sqlite:///:memory:")
  72. BizAttachment.__table__.create(engine)
  73. db = sessionmaker(bind=engine)()
  74. db.add(
  75. BizAttachment(
  76. id="att1",
  77. original_name="note.pdf",
  78. file_url="oa-approval/note.pdf",
  79. tenant_no="t1",
  80. create_dept_id="",
  81. )
  82. )
  83. db.commit()
  84. out = normalize_attachment_refs(
  85. None,
  86. {},
  87. [{"source": "upload", "url": "oa-approval/note.pdf", "fileName": "hack.pdf"}],
  88. db=db,
  89. tenant_no="t1",
  90. )
  91. self.assertEqual(out[0]["source"], "upload")
  92. self.assertEqual(out[0]["fileName"], "note.pdf")
  93. self.assertEqual(out[0]["fieldKey"], "")
  94. db.close()
  95. def test_rejects_upload_from_other_tenant_or_unknown_url(self):
  96. from sqlalchemy import create_engine
  97. from sqlalchemy.orm import sessionmaker
  98. from app.models.models import BizAttachment
  99. from app.services.oa_engine import OaEngineError
  100. engine = create_engine("sqlite:///:memory:")
  101. BizAttachment.__table__.create(engine)
  102. db = sessionmaker(bind=engine)()
  103. db.add(
  104. BizAttachment(
  105. id="att2",
  106. original_name="note.pdf",
  107. file_url="oa-approval/note.pdf",
  108. tenant_no="other",
  109. create_dept_id="",
  110. )
  111. )
  112. db.commit()
  113. with self.assertRaises(OaEngineError):
  114. normalize_attachment_refs(
  115. None,
  116. {},
  117. [{"source": "upload", "url": "oa-approval/note.pdf"}],
  118. db=db,
  119. tenant_no="t1",
  120. )
  121. with self.assertRaises(OaEngineError):
  122. normalize_attachment_refs(
  123. None,
  124. {},
  125. [{"source": "upload", "url": "https://evil.example/a.pdf"}],
  126. db=db,
  127. tenant_no="t1",
  128. )
  129. db.close()
  130. class TestCommentCrud(unittest.TestCase):
  131. def setUp(self):
  132. from sqlalchemy import create_engine
  133. from sqlalchemy.orm import sessionmaker
  134. from app.models.models import OaAppVersion, OaInstance, OaInstanceComment, SysUser
  135. self.engine = create_engine("sqlite:///:memory:")
  136. OaAppVersion.__table__.create(self.engine)
  137. OaInstance.__table__.create(self.engine)
  138. OaInstanceComment.__table__.create(self.engine)
  139. SysUser.__table__.create(self.engine)
  140. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  141. self.db = Session()
  142. now = datetime.now()
  143. schema = {
  144. "rule": [
  145. {"type": "upload", "field": "files", "title": "附件"},
  146. ]
  147. }
  148. self.db.add(
  149. OaAppVersion(
  150. id="v1",
  151. app_id="app1",
  152. version_no=1,
  153. status="published",
  154. is_latest_published="1",
  155. form_schema_json=json.dumps(schema, ensure_ascii=False),
  156. flow_tree_json="{}",
  157. advanced_settings_json="{}",
  158. tenant_no="zkxw",
  159. create_dept_id="",
  160. create_time=now,
  161. update_time=now,
  162. del_flag=0,
  163. )
  164. )
  165. self.db.add(
  166. OaInstance(
  167. id="i1",
  168. app_id="app1",
  169. app_version_id="v1",
  170. title="t",
  171. status="running",
  172. initiator_user_id="u0",
  173. initiator_dept_id="d1",
  174. form_data_json=json.dumps(
  175. {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]},
  176. ensure_ascii=False,
  177. ),
  178. tenant_no="zkxw",
  179. create_dept_id="d1",
  180. create_time=now,
  181. update_time=now,
  182. del_flag=0,
  183. current_round=1,
  184. )
  185. )
  186. self.db.add(
  187. SysUser(
  188. id="u1",
  189. tenant_no="zkxw",
  190. user_no="u1",
  191. nick_name="张三",
  192. email="",
  193. phone_no="",
  194. password="",
  195. status="1",
  196. create_dept_id="",
  197. del_flag=0,
  198. create_time=now,
  199. update_time=now,
  200. )
  201. )
  202. self.db.commit()
  203. def test_create_list_revoke(self):
  204. from app.services import oa_instance_comment as svc
  205. row = svc.create_comment(
  206. self.db,
  207. instance_id="i1",
  208. tenant_no="zkxw",
  209. user_id="u1",
  210. dept_id="d1",
  211. content="请看附件",
  212. attachment_refs=[{"fieldKey": "files", "url": "https://x/a.pdf"}],
  213. )
  214. self.assertEqual(row["status"], "normal")
  215. self.assertTrue(row["canRevoke"])
  216. self.assertEqual(len(row["attachmentRefs"]), 1)
  217. listed = svc.list_comments(
  218. self.db, instance_id="i1", tenant_no="zkxw", viewer_user_id="u1"
  219. )
  220. self.assertEqual(len(listed), 1)
  221. revoked = svc.revoke_comment(
  222. self.db,
  223. instance_id="i1",
  224. comment_id=row["id"],
  225. tenant_no="zkxw",
  226. user_id="u1",
  227. )
  228. self.assertEqual(revoked["status"], "revoked")
  229. self.assertFalse(revoked["canRevoke"])
  230. again = svc.revoke_comment(
  231. self.db,
  232. instance_id="i1",
  233. comment_id=row["id"],
  234. tenant_no="zkxw",
  235. user_id="u1",
  236. )
  237. self.assertEqual(again["status"], "revoked")
  238. def test_can_comment_and_revoke_when_completed(self):
  239. from app.services import oa_instance_comment as svc
  240. from app.models.models import OaInstance
  241. inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one()
  242. inst.status = "completed"
  243. self.db.commit()
  244. row = svc.create_comment(
  245. self.db,
  246. instance_id="i1",
  247. tenant_no="zkxw",
  248. user_id="u1",
  249. dept_id="d1",
  250. content="结束后仍可评论",
  251. attachment_refs=None,
  252. )
  253. self.assertEqual(row["status"], "normal")
  254. self.assertTrue(row["canRevoke"])
  255. revoked = svc.revoke_comment(
  256. self.db,
  257. instance_id="i1",
  258. comment_id=row["id"],
  259. tenant_no="zkxw",
  260. user_id="u1",
  261. )
  262. self.assertEqual(revoked["status"], "revoked")
  263. class TestCommentMentionGrant(unittest.TestCase):
  264. def setUp(self):
  265. from sqlalchemy import create_engine
  266. from sqlalchemy.orm import sessionmaker
  267. from app.models.models import (
  268. OaAppVersion,
  269. OaInstance,
  270. OaInstanceComment,
  271. OaInstanceViewGrant,
  272. OaTask,
  273. SysUser,
  274. )
  275. self.engine = create_engine("sqlite:///:memory:")
  276. for table in (
  277. OaAppVersion.__table__,
  278. OaInstance.__table__,
  279. OaInstanceComment.__table__,
  280. OaInstanceViewGrant.__table__,
  281. OaTask.__table__,
  282. SysUser.__table__,
  283. ):
  284. table.create(self.engine)
  285. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  286. self.db = Session()
  287. now = datetime.now()
  288. self.db.add(
  289. OaAppVersion(
  290. id="v1",
  291. app_id="app1",
  292. version_no=1,
  293. status="published",
  294. is_latest_published="1",
  295. form_schema_json="{}",
  296. flow_tree_json="{}",
  297. advanced_settings_json="{}",
  298. tenant_no="zkxw",
  299. create_dept_id="",
  300. create_time=now,
  301. update_time=now,
  302. del_flag=0,
  303. )
  304. )
  305. self.db.add(
  306. OaInstance(
  307. id="i1",
  308. app_id="app1",
  309. app_version_id="v1",
  310. title="t",
  311. status="running",
  312. initiator_user_id="u0",
  313. initiator_dept_id="d1",
  314. form_data_json="{}",
  315. tenant_no="zkxw",
  316. create_dept_id="d1",
  317. create_time=now,
  318. update_time=now,
  319. del_flag=0,
  320. current_round=1,
  321. )
  322. )
  323. for uid, name, status in (("u1", "张三", "1"), ("u2", "李四", "1"), ("u3", "王五", "0")):
  324. self.db.add(
  325. SysUser(
  326. id=uid,
  327. tenant_no="zkxw",
  328. user_no=uid,
  329. nick_name=name,
  330. email="",
  331. phone_no="",
  332. password="",
  333. status=status,
  334. create_dept_id="",
  335. del_flag=0,
  336. create_time=now,
  337. update_time=now,
  338. )
  339. )
  340. self.db.commit()
  341. def test_mention_grants_view_and_is_idempotent(self):
  342. from unittest.mock import patch
  343. from app.models.models import OaInstanceViewGrant
  344. from app.services import oa_instance_comment as svc
  345. with patch(
  346. "app.services.oa_wechat_notify.schedule_oa_mention_notifies"
  347. ) as schedule:
  348. row = svc.create_comment(
  349. self.db,
  350. instance_id="i1",
  351. tenant_no="zkxw",
  352. user_id="u1",
  353. dept_id="d1",
  354. content="",
  355. attachment_refs=None,
  356. mention_user_ids=["u2", "u2", "u1"],
  357. )
  358. schedule.assert_called_once_with(
  359. instance_id="i1",
  360. commenter_user_id="u1",
  361. mentioned_user_ids=["u2"],
  362. )
  363. self.assertEqual(row["content"], "")
  364. self.assertEqual([m["userId"] for m in row["mentions"]], ["u2"])
  365. self.assertEqual(row["mentions"][0]["userName"], "李四")
  366. grants = (
  367. self.db.query(OaInstanceViewGrant)
  368. .filter(OaInstanceViewGrant.del_flag == 0)
  369. .all()
  370. )
  371. self.assertEqual(len(grants), 1)
  372. self.assertEqual(grants[0].user_id, "u2")
  373. self.assertEqual(grants[0].source, "comment_mention")
  374. svc.create_comment(
  375. self.db,
  376. instance_id="i1",
  377. tenant_no="zkxw",
  378. user_id="u1",
  379. dept_id="d1",
  380. content="再提一次",
  381. attachment_refs=None,
  382. mention_user_ids=["u2"],
  383. )
  384. grants = (
  385. self.db.query(OaInstanceViewGrant)
  386. .filter(OaInstanceViewGrant.del_flag == 0)
  387. .all()
  388. )
  389. self.assertEqual(len(grants), 1)
  390. def test_rejects_empty_without_mention(self):
  391. from app.services import oa_instance_comment as svc
  392. from app.services.oa_engine import OaEngineError
  393. with self.assertRaises(OaEngineError):
  394. svc.create_comment(
  395. self.db,
  396. instance_id="i1",
  397. tenant_no="zkxw",
  398. user_id="u1",
  399. dept_id="d1",
  400. content=" ",
  401. attachment_refs=None,
  402. mention_user_ids=None,
  403. )
  404. def test_allows_upload_only_comment(self):
  405. from app.models.models import BizAttachment
  406. from app.services import oa_instance_comment as svc
  407. BizAttachment.__table__.create(self.engine)
  408. self.db.add(
  409. BizAttachment(
  410. id="att-c1",
  411. original_name="note.pdf",
  412. file_url="oa-approval/note.pdf",
  413. tenant_no="zkxw",
  414. create_dept_id="",
  415. )
  416. )
  417. self.db.commit()
  418. row = svc.create_comment(
  419. self.db,
  420. instance_id="i1",
  421. tenant_no="zkxw",
  422. user_id="u1",
  423. dept_id="d1",
  424. content=" ",
  425. attachment_refs=[{"source": "upload", "url": "oa-approval/note.pdf"}],
  426. mention_user_ids=None,
  427. )
  428. self.assertEqual(row["content"], "")
  429. self.assertEqual(row["attachmentRefs"][0]["source"], "upload")
  430. self.assertEqual(row["attachmentRefs"][0]["fileName"], "note.pdf")
  431. def test_rejects_disabled_user(self):
  432. from app.services import oa_instance_comment as svc
  433. from app.services.oa_engine import OaEngineError
  434. with self.assertRaises(OaEngineError):
  435. svc.create_comment(
  436. self.db,
  437. instance_id="i1",
  438. tenant_no="zkxw",
  439. user_id="u1",
  440. dept_id="d1",
  441. content="x",
  442. attachment_refs=None,
  443. mention_user_ids=["u3"],
  444. )
  445. def test_revoke_keeps_grant_and_list_skips_participants(self):
  446. from app.models.models import OaInstance, OaInstanceViewGrant, OaTask
  447. from app.services import oa_instance_comment as svc
  448. row = svc.create_comment(
  449. self.db,
  450. instance_id="i1",
  451. tenant_no="zkxw",
  452. user_id="u1",
  453. dept_id="d1",
  454. content="请看",
  455. attachment_refs=None,
  456. mention_user_ids=["u2"],
  457. )
  458. svc.revoke_comment(
  459. self.db,
  460. instance_id="i1",
  461. comment_id=row["id"],
  462. tenant_no="zkxw",
  463. user_id="u1",
  464. )
  465. self.assertEqual(
  466. self.db.query(OaInstanceViewGrant)
  467. .filter(OaInstanceViewGrant.del_flag == 0)
  468. .count(),
  469. 1,
  470. )
  471. self.assertEqual(
  472. svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
  473. ["i1"],
  474. )
  475. now = datetime.now()
  476. self.db.add(
  477. OaTask(
  478. id="t-cc",
  479. instance_id="i1",
  480. task_type="cc",
  481. assignee_user_id="u2",
  482. status="done",
  483. tenant_no="zkxw",
  484. create_dept_id="",
  485. create_time=now,
  486. update_time=now,
  487. del_flag=0,
  488. round_no=1,
  489. is_read=0,
  490. )
  491. )
  492. self.db.commit()
  493. # 即使已是抄送人,被@仍应出现在「提到我的」
  494. self.assertEqual(
  495. svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
  496. ["i1"],
  497. )
  498. inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one()
  499. inst.initiator_user_id = "u2"
  500. self.db.commit()
  501. # 即使已是发起人,被@仍应出现在「提到我的」
  502. self.assertEqual(
  503. svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
  504. ["i1"],
  505. )