test_oa_instance_comment.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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 app.models.models import OaInstanceViewGrant
  343. from app.services import oa_instance_comment as svc
  344. row = svc.create_comment(
  345. self.db,
  346. instance_id="i1",
  347. tenant_no="zkxw",
  348. user_id="u1",
  349. dept_id="d1",
  350. content="",
  351. attachment_refs=None,
  352. mention_user_ids=["u2", "u2", "u1"],
  353. )
  354. self.assertEqual(row["content"], "")
  355. self.assertEqual([m["userId"] for m in row["mentions"]], ["u2"])
  356. self.assertEqual(row["mentions"][0]["userName"], "李四")
  357. grants = (
  358. self.db.query(OaInstanceViewGrant)
  359. .filter(OaInstanceViewGrant.del_flag == 0)
  360. .all()
  361. )
  362. self.assertEqual(len(grants), 1)
  363. self.assertEqual(grants[0].user_id, "u2")
  364. self.assertEqual(grants[0].source, "comment_mention")
  365. svc.create_comment(
  366. self.db,
  367. instance_id="i1",
  368. tenant_no="zkxw",
  369. user_id="u1",
  370. dept_id="d1",
  371. content="再提一次",
  372. attachment_refs=None,
  373. mention_user_ids=["u2"],
  374. )
  375. grants = (
  376. self.db.query(OaInstanceViewGrant)
  377. .filter(OaInstanceViewGrant.del_flag == 0)
  378. .all()
  379. )
  380. self.assertEqual(len(grants), 1)
  381. def test_rejects_empty_without_mention(self):
  382. from app.services import oa_instance_comment as svc
  383. from app.services.oa_engine import OaEngineError
  384. with self.assertRaises(OaEngineError):
  385. svc.create_comment(
  386. self.db,
  387. instance_id="i1",
  388. tenant_no="zkxw",
  389. user_id="u1",
  390. dept_id="d1",
  391. content=" ",
  392. attachment_refs=None,
  393. mention_user_ids=None,
  394. )
  395. def test_allows_upload_only_comment(self):
  396. from app.models.models import BizAttachment
  397. from app.services import oa_instance_comment as svc
  398. BizAttachment.__table__.create(self.engine)
  399. self.db.add(
  400. BizAttachment(
  401. id="att-c1",
  402. original_name="note.pdf",
  403. file_url="oa-approval/note.pdf",
  404. tenant_no="zkxw",
  405. create_dept_id="",
  406. )
  407. )
  408. self.db.commit()
  409. row = svc.create_comment(
  410. self.db,
  411. instance_id="i1",
  412. tenant_no="zkxw",
  413. user_id="u1",
  414. dept_id="d1",
  415. content=" ",
  416. attachment_refs=[{"source": "upload", "url": "oa-approval/note.pdf"}],
  417. mention_user_ids=None,
  418. )
  419. self.assertEqual(row["content"], "")
  420. self.assertEqual(row["attachmentRefs"][0]["source"], "upload")
  421. self.assertEqual(row["attachmentRefs"][0]["fileName"], "note.pdf")
  422. def test_rejects_disabled_user(self):
  423. from app.services import oa_instance_comment as svc
  424. from app.services.oa_engine import OaEngineError
  425. with self.assertRaises(OaEngineError):
  426. svc.create_comment(
  427. self.db,
  428. instance_id="i1",
  429. tenant_no="zkxw",
  430. user_id="u1",
  431. dept_id="d1",
  432. content="x",
  433. attachment_refs=None,
  434. mention_user_ids=["u3"],
  435. )
  436. def test_revoke_keeps_grant_and_list_skips_participants(self):
  437. from app.models.models import OaInstance, OaInstanceViewGrant, OaTask
  438. from app.services import oa_instance_comment as svc
  439. row = svc.create_comment(
  440. self.db,
  441. instance_id="i1",
  442. tenant_no="zkxw",
  443. user_id="u1",
  444. dept_id="d1",
  445. content="请看",
  446. attachment_refs=None,
  447. mention_user_ids=["u2"],
  448. )
  449. svc.revoke_comment(
  450. self.db,
  451. instance_id="i1",
  452. comment_id=row["id"],
  453. tenant_no="zkxw",
  454. user_id="u1",
  455. )
  456. self.assertEqual(
  457. self.db.query(OaInstanceViewGrant)
  458. .filter(OaInstanceViewGrant.del_flag == 0)
  459. .count(),
  460. 1,
  461. )
  462. self.assertEqual(
  463. svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
  464. ["i1"],
  465. )
  466. now = datetime.now()
  467. self.db.add(
  468. OaTask(
  469. id="t-cc",
  470. instance_id="i1",
  471. task_type="cc",
  472. assignee_user_id="u2",
  473. status="done",
  474. tenant_no="zkxw",
  475. create_dept_id="",
  476. create_time=now,
  477. update_time=now,
  478. del_flag=0,
  479. round_no=1,
  480. is_read=0,
  481. )
  482. )
  483. self.db.commit()
  484. # 即使已是抄送人,被@仍应出现在「提到我的」
  485. self.assertEqual(
  486. svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
  487. ["i1"],
  488. )
  489. inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one()
  490. inst.initiator_user_id = "u2"
  491. self.db.commit()
  492. # 即使已是发起人,被@仍应出现在「提到我的」
  493. self.assertEqual(
  494. svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
  495. ["i1"],
  496. )