test_oa_instance_comment.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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_cannot_comment_when_not_running(self):
  239. from app.services import oa_instance_comment as svc
  240. from app.services.oa_engine import OaEngineError
  241. from app.models.models import OaInstance
  242. inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one()
  243. inst.status = "completed"
  244. self.db.commit()
  245. with self.assertRaises(OaEngineError):
  246. svc.create_comment(
  247. self.db,
  248. instance_id="i1",
  249. tenant_no="zkxw",
  250. user_id="u1",
  251. dept_id="d1",
  252. content="x",
  253. attachment_refs=None,
  254. )
  255. class TestCommentMentionGrant(unittest.TestCase):
  256. def setUp(self):
  257. from sqlalchemy import create_engine
  258. from sqlalchemy.orm import sessionmaker
  259. from app.models.models import (
  260. OaAppVersion,
  261. OaInstance,
  262. OaInstanceComment,
  263. OaInstanceViewGrant,
  264. OaTask,
  265. SysUser,
  266. )
  267. self.engine = create_engine("sqlite:///:memory:")
  268. for table in (
  269. OaAppVersion.__table__,
  270. OaInstance.__table__,
  271. OaInstanceComment.__table__,
  272. OaInstanceViewGrant.__table__,
  273. OaTask.__table__,
  274. SysUser.__table__,
  275. ):
  276. table.create(self.engine)
  277. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  278. self.db = Session()
  279. now = datetime.now()
  280. self.db.add(
  281. OaAppVersion(
  282. id="v1",
  283. app_id="app1",
  284. version_no=1,
  285. status="published",
  286. is_latest_published="1",
  287. form_schema_json="{}",
  288. flow_tree_json="{}",
  289. advanced_settings_json="{}",
  290. tenant_no="zkxw",
  291. create_dept_id="",
  292. create_time=now,
  293. update_time=now,
  294. del_flag=0,
  295. )
  296. )
  297. self.db.add(
  298. OaInstance(
  299. id="i1",
  300. app_id="app1",
  301. app_version_id="v1",
  302. title="t",
  303. status="running",
  304. initiator_user_id="u0",
  305. initiator_dept_id="d1",
  306. form_data_json="{}",
  307. tenant_no="zkxw",
  308. create_dept_id="d1",
  309. create_time=now,
  310. update_time=now,
  311. del_flag=0,
  312. current_round=1,
  313. )
  314. )
  315. for uid, name, status in (("u1", "张三", "1"), ("u2", "李四", "1"), ("u3", "王五", "0")):
  316. self.db.add(
  317. SysUser(
  318. id=uid,
  319. tenant_no="zkxw",
  320. user_no=uid,
  321. nick_name=name,
  322. email="",
  323. phone_no="",
  324. password="",
  325. status=status,
  326. create_dept_id="",
  327. del_flag=0,
  328. create_time=now,
  329. update_time=now,
  330. )
  331. )
  332. self.db.commit()
  333. def test_mention_grants_view_and_is_idempotent(self):
  334. from app.models.models import OaInstanceViewGrant
  335. from app.services import oa_instance_comment as svc
  336. row = svc.create_comment(
  337. self.db,
  338. instance_id="i1",
  339. tenant_no="zkxw",
  340. user_id="u1",
  341. dept_id="d1",
  342. content="",
  343. attachment_refs=None,
  344. mention_user_ids=["u2", "u2", "u1"],
  345. )
  346. self.assertEqual(row["content"], "")
  347. self.assertEqual([m["userId"] for m in row["mentions"]], ["u2"])
  348. self.assertEqual(row["mentions"][0]["userName"], "李四")
  349. grants = (
  350. self.db.query(OaInstanceViewGrant)
  351. .filter(OaInstanceViewGrant.del_flag == 0)
  352. .all()
  353. )
  354. self.assertEqual(len(grants), 1)
  355. self.assertEqual(grants[0].user_id, "u2")
  356. self.assertEqual(grants[0].source, "comment_mention")
  357. svc.create_comment(
  358. self.db,
  359. instance_id="i1",
  360. tenant_no="zkxw",
  361. user_id="u1",
  362. dept_id="d1",
  363. content="再提一次",
  364. attachment_refs=None,
  365. mention_user_ids=["u2"],
  366. )
  367. grants = (
  368. self.db.query(OaInstanceViewGrant)
  369. .filter(OaInstanceViewGrant.del_flag == 0)
  370. .all()
  371. )
  372. self.assertEqual(len(grants), 1)
  373. def test_rejects_empty_without_mention(self):
  374. from app.services import oa_instance_comment as svc
  375. from app.services.oa_engine import OaEngineError
  376. with self.assertRaises(OaEngineError):
  377. svc.create_comment(
  378. self.db,
  379. instance_id="i1",
  380. tenant_no="zkxw",
  381. user_id="u1",
  382. dept_id="d1",
  383. content=" ",
  384. attachment_refs=None,
  385. mention_user_ids=None,
  386. )
  387. def test_allows_upload_only_comment(self):
  388. from app.models.models import BizAttachment
  389. from app.services import oa_instance_comment as svc
  390. BizAttachment.__table__.create(self.engine)
  391. self.db.add(
  392. BizAttachment(
  393. id="att-c1",
  394. original_name="note.pdf",
  395. file_url="oa-approval/note.pdf",
  396. tenant_no="zkxw",
  397. create_dept_id="",
  398. )
  399. )
  400. self.db.commit()
  401. row = svc.create_comment(
  402. self.db,
  403. instance_id="i1",
  404. tenant_no="zkxw",
  405. user_id="u1",
  406. dept_id="d1",
  407. content=" ",
  408. attachment_refs=[{"source": "upload", "url": "oa-approval/note.pdf"}],
  409. mention_user_ids=None,
  410. )
  411. self.assertEqual(row["content"], "")
  412. self.assertEqual(row["attachmentRefs"][0]["source"], "upload")
  413. self.assertEqual(row["attachmentRefs"][0]["fileName"], "note.pdf")
  414. def test_rejects_disabled_user(self):
  415. from app.services import oa_instance_comment as svc
  416. from app.services.oa_engine import OaEngineError
  417. with self.assertRaises(OaEngineError):
  418. svc.create_comment(
  419. self.db,
  420. instance_id="i1",
  421. tenant_no="zkxw",
  422. user_id="u1",
  423. dept_id="d1",
  424. content="x",
  425. attachment_refs=None,
  426. mention_user_ids=["u3"],
  427. )
  428. def test_revoke_keeps_grant_and_list_skips_participants(self):
  429. from app.models.models import OaInstance, OaInstanceViewGrant, OaTask
  430. from app.services import oa_instance_comment as svc
  431. row = svc.create_comment(
  432. self.db,
  433. instance_id="i1",
  434. tenant_no="zkxw",
  435. user_id="u1",
  436. dept_id="d1",
  437. content="请看",
  438. attachment_refs=None,
  439. mention_user_ids=["u2"],
  440. )
  441. svc.revoke_comment(
  442. self.db,
  443. instance_id="i1",
  444. comment_id=row["id"],
  445. tenant_no="zkxw",
  446. user_id="u1",
  447. )
  448. self.assertEqual(
  449. self.db.query(OaInstanceViewGrant)
  450. .filter(OaInstanceViewGrant.del_flag == 0)
  451. .count(),
  452. 1,
  453. )
  454. self.assertEqual(
  455. svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
  456. ["i1"],
  457. )
  458. now = datetime.now()
  459. self.db.add(
  460. OaTask(
  461. id="t-cc",
  462. instance_id="i1",
  463. task_type="cc",
  464. assignee_user_id="u2",
  465. status="done",
  466. tenant_no="zkxw",
  467. create_dept_id="",
  468. create_time=now,
  469. update_time=now,
  470. del_flag=0,
  471. round_no=1,
  472. is_read=0,
  473. )
  474. )
  475. self.db.commit()
  476. # 即使已是抄送人,被@仍应出现在「提到我的」
  477. self.assertEqual(
  478. svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"),
  479. ["i1"],
  480. )
  481. inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one()
  482. inst.initiator_user_id = "u2"
  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. )