"""OA 实例评论:附件枚举与引用校验""" import json import unittest from datetime import datetime from app.services.oa_instance_comment import ( enumerate_form_attachments, normalize_attachment_refs, ) class TestEnumerateFormAttachments(unittest.TestCase): def test_groups_upload_fields(self): schema = { "rule": [ { "type": "upload", "field": "files", "title": "附件", }, {"type": "input", "field": "name", "title": "名称"}, ] } form = { "files": [ {"url": "https://x/a.pdf", "name": "a.pdf"}, {"url": "https://x/b.pdf", "name": "b.pdf"}, ], "name": "demo", } items = enumerate_form_attachments(schema, form) self.assertEqual(len(items), 1) self.assertEqual(items[0]["fieldKey"], "files") self.assertEqual(items[0]["fieldLabel"], "附件") self.assertEqual(len(items[0]["files"]), 2) self.assertEqual(items[0]["files"][0]["fileName"], "a.pdf") def test_skips_empty_upload(self): schema = {"rule": [{"type": "fcUpload", "field": "f", "title": "F"}]} self.assertEqual(enumerate_form_attachments(schema, {"f": []}), []) class TestNormalizeAttachmentRefs(unittest.TestCase): def test_accepts_valid_ref(self): schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]} form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]} out = normalize_attachment_refs( schema, form, [{"fieldKey": "files", "url": "https://x/a.pdf", "fileName": "wrong"}], ) self.assertEqual(out[0]["fileName"], "a.pdf") self.assertEqual(out[0]["fieldLabel"], "附件") def test_rejects_foreign_url(self): from app.services.oa_engine import OaEngineError schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]} form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]} with self.assertRaises(OaEngineError): normalize_attachment_refs( schema, form, [{"fieldKey": "files", "url": "https://evil/x.pdf"}], ) def test_marks_form_ref_source(self): schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]} form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]} out = normalize_attachment_refs( schema, form, [{"url": "https://x/a.pdf"}], ) self.assertEqual(out[0]["source"], "form") def test_accepts_tenant_upload_and_uses_stored_name(self): from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.models.models import BizAttachment engine = create_engine("sqlite:///:memory:") BizAttachment.__table__.create(engine) db = sessionmaker(bind=engine)() db.add( BizAttachment( id="att1", original_name="note.pdf", file_url="oa-approval/note.pdf", tenant_no="t1", create_dept_id="", ) ) db.commit() out = normalize_attachment_refs( None, {}, [{"source": "upload", "url": "oa-approval/note.pdf", "fileName": "hack.pdf"}], db=db, tenant_no="t1", ) self.assertEqual(out[0]["source"], "upload") self.assertEqual(out[0]["fileName"], "note.pdf") self.assertEqual(out[0]["fieldKey"], "") db.close() def test_rejects_upload_from_other_tenant_or_unknown_url(self): from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.models.models import BizAttachment from app.services.oa_engine import OaEngineError engine = create_engine("sqlite:///:memory:") BizAttachment.__table__.create(engine) db = sessionmaker(bind=engine)() db.add( BizAttachment( id="att2", original_name="note.pdf", file_url="oa-approval/note.pdf", tenant_no="other", create_dept_id="", ) ) db.commit() with self.assertRaises(OaEngineError): normalize_attachment_refs( None, {}, [{"source": "upload", "url": "oa-approval/note.pdf"}], db=db, tenant_no="t1", ) with self.assertRaises(OaEngineError): normalize_attachment_refs( None, {}, [{"source": "upload", "url": "https://evil.example/a.pdf"}], db=db, tenant_no="t1", ) db.close() class TestCommentCrud(unittest.TestCase): def setUp(self): from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.models.models import OaAppVersion, OaInstance, OaInstanceComment, SysUser self.engine = create_engine("sqlite:///:memory:") OaAppVersion.__table__.create(self.engine) OaInstance.__table__.create(self.engine) OaInstanceComment.__table__.create(self.engine) SysUser.__table__.create(self.engine) Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False) self.db = Session() now = datetime.now() schema = { "rule": [ {"type": "upload", "field": "files", "title": "附件"}, ] } self.db.add( OaAppVersion( id="v1", app_id="app1", version_no=1, status="published", is_latest_published="1", form_schema_json=json.dumps(schema, ensure_ascii=False), flow_tree_json="{}", advanced_settings_json="{}", tenant_no="zkxw", create_dept_id="", create_time=now, update_time=now, del_flag=0, ) ) self.db.add( OaInstance( id="i1", app_id="app1", app_version_id="v1", title="t", status="running", initiator_user_id="u0", initiator_dept_id="d1", form_data_json=json.dumps( {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]}, ensure_ascii=False, ), tenant_no="zkxw", create_dept_id="d1", create_time=now, update_time=now, del_flag=0, current_round=1, ) ) self.db.add( SysUser( id="u1", tenant_no="zkxw", user_no="u1", nick_name="张三", email="", phone_no="", password="", status="1", create_dept_id="", del_flag=0, create_time=now, update_time=now, ) ) self.db.commit() def test_create_list_revoke(self): from app.services import oa_instance_comment as svc row = svc.create_comment( self.db, instance_id="i1", tenant_no="zkxw", user_id="u1", dept_id="d1", content="请看附件", attachment_refs=[{"fieldKey": "files", "url": "https://x/a.pdf"}], ) self.assertEqual(row["status"], "normal") self.assertTrue(row["canRevoke"]) self.assertEqual(len(row["attachmentRefs"]), 1) listed = svc.list_comments( self.db, instance_id="i1", tenant_no="zkxw", viewer_user_id="u1" ) self.assertEqual(len(listed), 1) revoked = svc.revoke_comment( self.db, instance_id="i1", comment_id=row["id"], tenant_no="zkxw", user_id="u1", ) self.assertEqual(revoked["status"], "revoked") self.assertFalse(revoked["canRevoke"]) again = svc.revoke_comment( self.db, instance_id="i1", comment_id=row["id"], tenant_no="zkxw", user_id="u1", ) self.assertEqual(again["status"], "revoked") def test_can_comment_and_revoke_when_completed(self): from app.services import oa_instance_comment as svc from app.models.models import OaInstance inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one() inst.status = "completed" self.db.commit() row = svc.create_comment( self.db, instance_id="i1", tenant_no="zkxw", user_id="u1", dept_id="d1", content="结束后仍可评论", attachment_refs=None, ) self.assertEqual(row["status"], "normal") self.assertTrue(row["canRevoke"]) revoked = svc.revoke_comment( self.db, instance_id="i1", comment_id=row["id"], tenant_no="zkxw", user_id="u1", ) self.assertEqual(revoked["status"], "revoked") class TestCommentMentionGrant(unittest.TestCase): def setUp(self): from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.models.models import ( OaAppVersion, OaInstance, OaInstanceComment, OaInstanceViewGrant, OaTask, SysUser, ) self.engine = create_engine("sqlite:///:memory:") for table in ( OaAppVersion.__table__, OaInstance.__table__, OaInstanceComment.__table__, OaInstanceViewGrant.__table__, OaTask.__table__, SysUser.__table__, ): table.create(self.engine) Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False) self.db = Session() now = datetime.now() self.db.add( OaAppVersion( id="v1", app_id="app1", version_no=1, status="published", is_latest_published="1", form_schema_json="{}", flow_tree_json="{}", advanced_settings_json="{}", tenant_no="zkxw", create_dept_id="", create_time=now, update_time=now, del_flag=0, ) ) self.db.add( OaInstance( id="i1", app_id="app1", app_version_id="v1", title="t", status="running", initiator_user_id="u0", initiator_dept_id="d1", form_data_json="{}", tenant_no="zkxw", create_dept_id="d1", create_time=now, update_time=now, del_flag=0, current_round=1, ) ) for uid, name, status in (("u1", "张三", "1"), ("u2", "李四", "1"), ("u3", "王五", "0")): self.db.add( SysUser( id=uid, tenant_no="zkxw", user_no=uid, nick_name=name, email="", phone_no="", password="", status=status, create_dept_id="", del_flag=0, create_time=now, update_time=now, ) ) self.db.commit() def test_mention_grants_view_and_is_idempotent(self): from unittest.mock import patch from app.models.models import OaInstanceViewGrant from app.services import oa_instance_comment as svc with patch( "app.services.oa_wechat_notify.schedule_oa_mention_notifies" ) as schedule: row = svc.create_comment( self.db, instance_id="i1", tenant_no="zkxw", user_id="u1", dept_id="d1", content="", attachment_refs=None, mention_user_ids=["u2", "u2", "u1"], ) schedule.assert_called_once_with( instance_id="i1", commenter_user_id="u1", mentioned_user_ids=["u2"], ) self.assertEqual(row["content"], "") self.assertEqual([m["userId"] for m in row["mentions"]], ["u2"]) self.assertEqual(row["mentions"][0]["userName"], "李四") grants = ( self.db.query(OaInstanceViewGrant) .filter(OaInstanceViewGrant.del_flag == 0) .all() ) self.assertEqual(len(grants), 1) self.assertEqual(grants[0].user_id, "u2") self.assertEqual(grants[0].source, "comment_mention") svc.create_comment( self.db, instance_id="i1", tenant_no="zkxw", user_id="u1", dept_id="d1", content="再提一次", attachment_refs=None, mention_user_ids=["u2"], ) grants = ( self.db.query(OaInstanceViewGrant) .filter(OaInstanceViewGrant.del_flag == 0) .all() ) self.assertEqual(len(grants), 1) def test_rejects_empty_without_mention(self): from app.services import oa_instance_comment as svc from app.services.oa_engine import OaEngineError with self.assertRaises(OaEngineError): svc.create_comment( self.db, instance_id="i1", tenant_no="zkxw", user_id="u1", dept_id="d1", content=" ", attachment_refs=None, mention_user_ids=None, ) def test_allows_upload_only_comment(self): from app.models.models import BizAttachment from app.services import oa_instance_comment as svc BizAttachment.__table__.create(self.engine) self.db.add( BizAttachment( id="att-c1", original_name="note.pdf", file_url="oa-approval/note.pdf", tenant_no="zkxw", create_dept_id="", ) ) self.db.commit() row = svc.create_comment( self.db, instance_id="i1", tenant_no="zkxw", user_id="u1", dept_id="d1", content=" ", attachment_refs=[{"source": "upload", "url": "oa-approval/note.pdf"}], mention_user_ids=None, ) self.assertEqual(row["content"], "") self.assertEqual(row["attachmentRefs"][0]["source"], "upload") self.assertEqual(row["attachmentRefs"][0]["fileName"], "note.pdf") def test_rejects_disabled_user(self): from app.services import oa_instance_comment as svc from app.services.oa_engine import OaEngineError with self.assertRaises(OaEngineError): svc.create_comment( self.db, instance_id="i1", tenant_no="zkxw", user_id="u1", dept_id="d1", content="x", attachment_refs=None, mention_user_ids=["u3"], ) def test_revoke_keeps_grant_and_list_skips_participants(self): from app.models.models import OaInstance, OaInstanceViewGrant, OaTask from app.services import oa_instance_comment as svc row = svc.create_comment( self.db, instance_id="i1", tenant_no="zkxw", user_id="u1", dept_id="d1", content="请看", attachment_refs=None, mention_user_ids=["u2"], ) svc.revoke_comment( self.db, instance_id="i1", comment_id=row["id"], tenant_no="zkxw", user_id="u1", ) self.assertEqual( self.db.query(OaInstanceViewGrant) .filter(OaInstanceViewGrant.del_flag == 0) .count(), 1, ) self.assertEqual( svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"), ["i1"], ) now = datetime.now() self.db.add( OaTask( id="t-cc", instance_id="i1", task_type="cc", assignee_user_id="u2", status="done", tenant_no="zkxw", create_dept_id="", create_time=now, update_time=now, del_flag=0, round_no=1, is_read=0, ) ) self.db.commit() # 即使已是抄送人,被@仍应出现在「提到我的」 self.assertEqual( svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"), ["i1"], ) inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one() inst.initiator_user_id = "u2" self.db.commit() # 即使已是发起人,被@仍应出现在「提到我的」 self.assertEqual( svc.list_mentioned_instance_ids(self.db, tenant_no="zkxw", user_id="u2"), ["i1"], )