test_web_sensitive_permissions.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. import asyncio
  2. from pathlib import Path
  3. import pytest
  4. from fastapi import FastAPI, HTTPException
  5. from fastapi.testclient import TestClient
  6. from sqlalchemy import create_engine
  7. from sqlalchemy.orm import sessionmaker
  8. from sqlalchemy.pool import StaticPool
  9. from app.core import menu_guard
  10. from app.core.auth import get_current_user
  11. from app.core.database import get_db
  12. from app.models.base import Base
  13. from app.models.models import (
  14. SysMenu,
  15. SysPost,
  16. SysPostRole,
  17. SysRole,
  18. SysRoleMenu,
  19. SysUser,
  20. SysUserPost,
  21. SysUserRole,
  22. )
  23. PROTOCOL_LIST = "biz:cellStorageProtocol:list"
  24. PROTOCOL_AUDIT = "biz:cellStorageProtocolAudit:list"
  25. PAYMENT_CONFIRM = "biz:cellStoragePaymentConfirm:list"
  26. REFUND_CONFIRM = "biz:cellStorageRefundConfirm:list"
  27. STORAGE_CERT = "cell-storage:storage-cert:list"
  28. QC_MENUS = {"biz:qcWorkOrder:list", "biz:myQcTask:list"}
  29. STORAGE_CERT_QC_MENUS = {
  30. "biz:qcWorkOrder:list",
  31. "biz:myQcTask:list",
  32. "production:work-order:list",
  33. "production:my-task:list",
  34. "cell-storage:sample-info:list",
  35. }
  36. def _session():
  37. engine = create_engine("sqlite:///:memory:")
  38. Base.metadata.create_all(
  39. engine,
  40. tables=[
  41. SysMenu.__table__,
  42. SysPost.__table__,
  43. SysPostRole.__table__,
  44. SysRole.__table__,
  45. SysRoleMenu.__table__,
  46. SysUser.__table__,
  47. SysUserPost.__table__,
  48. SysUserRole.__table__,
  49. ],
  50. )
  51. return sessionmaker(bind=engine)()
  52. def _thread_session():
  53. engine = create_engine(
  54. "sqlite://",
  55. connect_args={"check_same_thread": False},
  56. poolclass=StaticPool,
  57. )
  58. Base.metadata.create_all(
  59. engine,
  60. tables=[
  61. SysMenu.__table__,
  62. SysPost.__table__,
  63. SysPostRole.__table__,
  64. SysRole.__table__,
  65. SysRoleMenu.__table__,
  66. SysUser.__table__,
  67. SysUserPost.__table__,
  68. SysUserRole.__table__,
  69. ],
  70. )
  71. return sessionmaker(bind=engine)()
  72. def _add_user(db, *, user_id="u1", tenant_no="tenant-a"):
  73. db.add(
  74. SysUser(
  75. id=user_id,
  76. user_no=user_id,
  77. nick_name=user_id,
  78. password="unused",
  79. status="1",
  80. tenant_no=tenant_no,
  81. create_dept_id="",
  82. del_flag=0,
  83. )
  84. )
  85. def _add_role(db, *, role_id, tenant_no, role_name="普通角色"):
  86. db.add(
  87. SysRole(
  88. id=role_id,
  89. role_name=role_name,
  90. status="1",
  91. tenant_no=tenant_no,
  92. create_dept_id="",
  93. del_flag=0,
  94. )
  95. )
  96. def _add_menu_grant(db, *, role_id, tenant_no, code, suffix):
  97. menu_id = f"menu-{suffix}"
  98. db.add_all(
  99. [
  100. SysMenu(
  101. id=menu_id,
  102. menu_name=code,
  103. parent_id="root",
  104. menu_type="C",
  105. path=f"/{suffix}",
  106. perms=code,
  107. status="1",
  108. tenant_no=tenant_no,
  109. create_dept_id="",
  110. del_flag=0,
  111. ),
  112. SysRoleMenu(
  113. id=f"rm-{role_id}-{suffix}",
  114. role_id=role_id,
  115. menu_id=menu_id,
  116. tenant_no=tenant_no,
  117. create_dept_id="",
  118. del_flag=0,
  119. ),
  120. ]
  121. )
  122. def _bind_direct(db, *, user_id, role_id, tenant_no, suffix):
  123. db.add(
  124. SysUserRole(
  125. id=f"ur-{suffix}",
  126. user_id=user_id,
  127. role_id=role_id,
  128. tenant_no=tenant_no,
  129. create_dept_id="",
  130. del_flag=0,
  131. )
  132. )
  133. def _bind_post(db, *, user_id, role_id, tenant_no, suffix):
  134. post_id = f"post-{suffix}"
  135. db.add_all(
  136. [
  137. SysPost(
  138. id=post_id,
  139. post_name="授权职位",
  140. post_code=post_id,
  141. status="1",
  142. tenant_no=tenant_no,
  143. create_dept_id="",
  144. del_flag=0,
  145. ),
  146. SysUserPost(
  147. id=f"up-{suffix}",
  148. user_id=user_id,
  149. post_id=post_id,
  150. tenant_no=tenant_no,
  151. create_dept_id="",
  152. del_flag=0,
  153. ),
  154. SysPostRole(
  155. id=f"pr-{suffix}",
  156. post_id=post_id,
  157. role_id=role_id,
  158. tenant_no=tenant_no,
  159. create_dept_id="",
  160. del_flag=0,
  161. ),
  162. ]
  163. )
  164. def _strict_checker():
  165. checker = getattr(menu_guard, "strict_user_has_menu_perm", None)
  166. assert callable(checker), "menu_guard must expose strict_user_has_menu_perm"
  167. return checker
  168. def _strict_dependency(code):
  169. factory = getattr(menu_guard, "require_strict_menu_code", None)
  170. assert callable(factory), "menu_guard must expose require_strict_menu_code"
  171. return factory(code)
  172. def test_strict_permission_uses_direct_and_post_role_union():
  173. db = _session()
  174. _add_user(db)
  175. _add_role(db, role_id="market", tenant_no="tenant-a", role_name="市场专员")
  176. _add_role(db, role_id="finance", tenant_no="tenant-a", role_name="财务人员")
  177. _add_menu_grant(
  178. db,
  179. role_id="market",
  180. tenant_no="tenant-a",
  181. code="mini:market:specialist:payment",
  182. suffix="mini",
  183. )
  184. _add_menu_grant(
  185. db,
  186. role_id="finance",
  187. tenant_no="tenant-a",
  188. code=PAYMENT_CONFIRM,
  189. suffix="finance",
  190. )
  191. _bind_direct(
  192. db, user_id="u1", role_id="market", tenant_no="tenant-a", suffix="market"
  193. )
  194. _bind_post(
  195. db, user_id="u1", role_id="finance", tenant_no="tenant-a", suffix="finance"
  196. )
  197. db.commit()
  198. checker = _strict_checker()
  199. user = {"user_id": "u1", "tenant_no": "forged", "role": "admin"}
  200. assert checker(db, user, PAYMENT_CONFIRM)
  201. assert not checker(db, user, REFUND_CONFIRM)
  202. assert asyncio.run(_strict_dependency(PAYMENT_CONFIRM)(user=user, db=db)) is user
  203. def test_strict_permission_ignores_cross_tenant_grants():
  204. db = _session()
  205. _add_user(db, tenant_no="tenant-a")
  206. _add_role(db, role_id="market", tenant_no="tenant-a", role_name="市场专员")
  207. _bind_direct(
  208. db, user_id="u1", role_id="market", tenant_no="tenant-a", suffix="market"
  209. )
  210. _add_role(db, role_id="finance-b", tenant_no="tenant-b", role_name="财务人员")
  211. _add_menu_grant(
  212. db,
  213. role_id="finance-b",
  214. tenant_no="tenant-b",
  215. code=PAYMENT_CONFIRM,
  216. suffix="finance-b",
  217. )
  218. _bind_direct(
  219. db,
  220. user_id="u1",
  221. role_id="finance-b",
  222. tenant_no="tenant-b",
  223. suffix="finance-b",
  224. )
  225. db.commit()
  226. assert not _strict_checker()(
  227. db,
  228. {"user_id": "u1", "tenant_no": "tenant-b", "role": "super_admin"},
  229. PAYMENT_CONFIRM,
  230. )
  231. @pytest.mark.parametrize("role_name", ["超级市场专员", "超级管理员"])
  232. def test_strict_permission_rejects_privileged_role_name_without_menu(role_name):
  233. db = _session()
  234. _add_user(db)
  235. _add_role(
  236. db,
  237. role_id="super",
  238. tenant_no="tenant-a",
  239. role_name=role_name,
  240. )
  241. _bind_direct(
  242. db, user_id="u1", role_id="super", tenant_no="tenant-a", suffix="super"
  243. )
  244. db.commit()
  245. user = {"user_id": "u1", "tenant_no": "forged", "role": "admin"}
  246. assert not _strict_checker()(db, user, REFUND_CONFIRM)
  247. with pytest.raises(HTTPException) as exc_info:
  248. asyncio.run(_strict_dependency(REFUND_CONFIRM)(user=user, db=db))
  249. assert exc_info.value.status_code == 403
  250. def test_strict_permission_allows_super_admin_only_with_explicit_menu_grant():
  251. db = _session()
  252. _add_user(db)
  253. _add_role(db, role_id="super", tenant_no="tenant-a", role_name="超级管理员")
  254. _add_menu_grant(
  255. db,
  256. role_id="super",
  257. tenant_no="tenant-a",
  258. code=REFUND_CONFIRM,
  259. suffix="refund",
  260. )
  261. _bind_direct(
  262. db, user_id="u1", role_id="super", tenant_no="tenant-a", suffix="super"
  263. )
  264. db.commit()
  265. user = {"user_id": "u1", "tenant_no": "forged", "role": "super_admin"}
  266. assert _strict_checker()(db, user, REFUND_CONFIRM)
  267. assert asyncio.run(_strict_dependency(REFUND_CONFIRM)(user=user, db=db)) is user
  268. def test_strict_dependency_rejects_market_only_role():
  269. db = _session()
  270. _add_user(db)
  271. _add_role(db, role_id="market", tenant_no="tenant-a", role_name="地区经理")
  272. _add_menu_grant(
  273. db,
  274. role_id="market",
  275. tenant_no="tenant-a",
  276. code="mini:market:manager:protocol",
  277. suffix="mini",
  278. )
  279. _bind_direct(
  280. db, user_id="u1", role_id="market", tenant_no="tenant-a", suffix="market"
  281. )
  282. db.commit()
  283. with pytest.raises(HTTPException) as exc_info:
  284. asyncio.run(
  285. _strict_dependency(PROTOCOL_AUDIT)(
  286. user={"user_id": "u1", "tenant_no": "tenant-a", "role": "admin"},
  287. db=db,
  288. )
  289. )
  290. assert exc_info.value.status_code == 403
  291. def _route_strict_codes(router, endpoint_name):
  292. route = next(r for r in router.routes if r.endpoint.__name__ == endpoint_name)
  293. codes = set()
  294. for dependency in route.dependant.dependencies:
  295. codes.update(getattr(dependency.call, "strict_menu_codes", ()))
  296. return codes
  297. @pytest.mark.parametrize(
  298. ("module_name", "endpoint_names", "expected_codes"),
  299. [
  300. (
  301. "research_donation",
  302. [
  303. "create_research_donation",
  304. "update_research_donation",
  305. ],
  306. set(),
  307. ),
  308. (
  309. "protocol_sample_payment",
  310. ["confirm_payment_transfer", "reject_payment_transfer"],
  311. set(),
  312. ),
  313. (
  314. "presign_order",
  315. ["confirm_presign_transfer", "reject_presign_transfer"],
  316. set(),
  317. ),
  318. (
  319. "sample_cert_insurance",
  320. ["put_sample_storage_cert", "put_sample_insurance"],
  321. set(),
  322. ),
  323. (
  324. "sample_storage_cert_qc",
  325. ["put_sample_storage_cert_qc"],
  326. STORAGE_CERT_QC_MENUS,
  327. ),
  328. ],
  329. )
  330. def test_sensitive_write_routes_declare_strict_menu_dependencies(
  331. module_name, endpoint_names, expected_codes
  332. ):
  333. module = __import__(f"app.api.{module_name}", fromlist=["router"])
  334. for endpoint_name in endpoint_names:
  335. assert _route_strict_codes(module.router, endpoint_name) == expected_codes
  336. def test_research_donation_audit_route_has_no_strict_menu_dependency():
  337. """科研捐赠审核只保留运营端登录,不再要求审核菜单权限。"""
  338. from app.api import research_donation
  339. assert _route_strict_codes(research_donation.router, "submit_donation_audit") == set()
  340. def test_collection_protocol_write_routes_have_no_strict_menu_dependencies():
  341. """采集协议单接口仅保留运营端登录守卫,不再挂菜单权限码。"""
  342. from app.api import collection_protocol
  343. write_endpoints = [
  344. "create_collection_protocol",
  345. "update_collection_protocol",
  346. "delete_collection_protocol",
  347. "submit_collection_protocol_audit",
  348. "post_receipt_confirm",
  349. "post_receipt_reject",
  350. "post_protocol_invoice_request",
  351. "complete_protocol_invoice_request",
  352. "post_submit_refund_finance",
  353. "post_submit_refund_finance_batch",
  354. "post_refund_confirm",
  355. "post_refund_reject",
  356. "update_fullmoon_todo_insurance",
  357. "update_storage_cert_handle",
  358. "update_fullmoon_todo_handle",
  359. ]
  360. for endpoint_name in write_endpoints:
  361. assert _route_strict_codes(collection_protocol.router, endpoint_name) == set()
  362. def test_protocol_refund_record_routes_have_no_strict_menu_dependencies():
  363. """采集协议单上的退费登记只保留运营端登录,不再要求退费确认菜单。"""
  364. from app.api import protocol_refund_record
  365. for endpoint_name in (
  366. "list_protocol_refund_records",
  367. "create_protocol_refund_record",
  368. "update_protocol_refund_record",
  369. "delete_protocol_refund_record",
  370. ):
  371. assert _route_strict_codes(protocol_refund_record.router, endpoint_name) == set()
  372. def test_collection_protocol_write_paths_keep_payment_detail_guard_only():
  373. source = Path("app/api/collection_protocol.py").read_text()
  374. assert "assert_protocol_sensitive_fields" in source
  375. assert "require_strict_menu_code" not in source
  376. assert "assert_strict_menu_code" not in source
  377. def test_research_donation_write_paths_assert_sensitive_field_permissions():
  378. source = Path("app/api/research_donation.py").read_text()
  379. assert "assert_donation_sensitive_fields" in source
  380. assert "require_strict_menu_code" not in source
  381. assert "assert_strict_menu_code" not in source
  382. def _permission_user(db, *codes):
  383. _add_user(db)
  384. _add_role(db, role_id="protocol", tenant_no="tenant-a", role_name="协议录入")
  385. _add_menu_grant(
  386. db,
  387. role_id="protocol",
  388. tenant_no="tenant-a",
  389. code=PROTOCOL_LIST,
  390. suffix="protocol",
  391. )
  392. _bind_direct(
  393. db,
  394. user_id="u1",
  395. role_id="protocol",
  396. tenant_no="tenant-a",
  397. suffix="protocol",
  398. )
  399. for index, code in enumerate(codes):
  400. role_id = f"extra-{index}"
  401. _add_role(db, role_id=role_id, tenant_no="tenant-a")
  402. _add_menu_grant(
  403. db,
  404. role_id=role_id,
  405. tenant_no="tenant-a",
  406. code=code,
  407. suffix=role_id,
  408. )
  409. _bind_direct(
  410. db,
  411. user_id="u1",
  412. role_id=role_id,
  413. tenant_no="tenant-a",
  414. suffix=role_id,
  415. )
  416. db.commit()
  417. return {"user_id": "u1", "tenant_no": "tenant-a", "role": "admin"}
  418. def test_protocol_sensitive_fields_no_longer_require_menu_permissions():
  419. from app.api.collection_protocol import (
  420. ProtocolCreateBody,
  421. ProtocolUpdateBody,
  422. assert_protocol_sensitive_fields,
  423. )
  424. db = _session()
  425. user = _permission_user(db)
  426. assert_protocol_sensitive_fields(db, user, ProtocolCreateBody(), creating=True)
  427. assert_protocol_sensitive_fields(
  428. db,
  429. user,
  430. ProtocolCreateBody(protocolStatus="sxz"),
  431. creating=True,
  432. )
  433. assert_protocol_sensitive_fields(
  434. db,
  435. user,
  436. ProtocolUpdateBody(storageCert={}),
  437. creating=False,
  438. )
  439. def test_donation_sensitive_fields_no_longer_require_menu_permissions():
  440. from app.api.research_donation import (
  441. DonationCreateBody,
  442. DonationUpdateBody,
  443. assert_donation_sensitive_fields,
  444. )
  445. db = _session()
  446. user = _permission_user(db)
  447. assert_donation_sensitive_fields(db, user, DonationCreateBody(), creating=True)
  448. for body, creating in (
  449. (DonationCreateBody(donationStatus="sxz"), True),
  450. (DonationCreateBody(sourceType="cell_storage"), True),
  451. (DonationUpdateBody(sourceProtocolId="p1"), False),
  452. ):
  453. assert_donation_sensitive_fields(db, user, body, creating=creating)
  454. def test_market_only_user_is_stopped_before_storage_cert_qc_write(monkeypatch):
  455. """储存证质控写接口仍挂严格菜单码:无对应菜单时应 403。"""
  456. from app.api import sample_storage_cert_qc
  457. db = _thread_session()
  458. _add_user(db)
  459. _add_role(db, role_id="market", tenant_no="tenant-a", role_name="市场专员")
  460. _add_menu_grant(
  461. db,
  462. role_id="market",
  463. tenant_no="tenant-a",
  464. code="mini:market:specialist:protocol",
  465. suffix="mini-protocol",
  466. )
  467. _bind_direct(
  468. db, user_id="u1", role_id="market", tenant_no="tenant-a", suffix="market"
  469. )
  470. db.commit()
  471. called = []
  472. def forbidden_call(*args, **kwargs):
  473. called.append((args, kwargs))
  474. raise AssertionError("sensitive CRUD must not be called")
  475. monkeypatch.setattr(sample_storage_cert_qc.svc, "upsert", forbidden_call)
  476. app = FastAPI()
  477. app.include_router(sample_storage_cert_qc.router)
  478. user = {"user_id": "u1", "tenant_no": "tenant-a", "role": "admin"}
  479. def override_db():
  480. yield db
  481. app.dependency_overrides[get_current_user] = lambda: user
  482. app.dependency_overrides[get_db] = override_db
  483. client = TestClient(app)
  484. response = client.request(
  485. "put", "/web/admin/sample-storage-cert-qc/S1", json={"payload": {}}
  486. )
  487. assert response.status_code == 403, response.text
  488. assert called == []
  489. def test_quick_register_writes_skip_menu_and_data_scope():
  490. """客户服务七类登记不再校验菜单码,也不按数据权限拦截样本。"""
  491. api_source = Path("app/api/protocol_sample_collection.py").read_text()
  492. crud_source = Path("app/crud/sample_info_quick_register.py").read_text()
  493. assert "assert_strict_menu_code" not in api_source
  494. assert "apply_data_scope" not in crud_source
  495. @pytest.mark.parametrize("kind", ["storage_cert", "insurance", "all"])
  496. def test_quick_register_does_not_require_storage_cert_menu(monkeypatch, kind):
  497. """客户服务管理登记储存证/保险不再要求储存证菜单权限。"""
  498. from app.api import protocol_sample_collection
  499. db = _session()
  500. user = _permission_user(db)
  501. called = []
  502. def allowed_call(*args, **kwargs):
  503. called.append((args, kwargs))
  504. return {"success": 1, "fail": 0}
  505. monkeypatch.setattr(
  506. protocol_sample_collection.quick_svc,
  507. "batch_handle_storage_certs",
  508. allowed_call,
  509. )
  510. monkeypatch.setattr(
  511. protocol_sample_collection.quick_svc,
  512. "batch_handle_insurance",
  513. allowed_call,
  514. )
  515. body = protocol_sample_collection.QuickRegisterBody(
  516. kind=kind,
  517. receptionSource="cell_storage",
  518. items=[{"sampleCode": "S1", "protocolId": "P1"}],
  519. handleTime="2026-10-14 10:00:00",
  520. )
  521. result = asyncio.run(
  522. protocol_sample_collection.quick_register_sample_info(
  523. body=body,
  524. db=db,
  525. user=user,
  526. )
  527. )
  528. assert result["code"] == 200
  529. assert called
  530. @pytest.mark.parametrize(
  531. "payment_detail",
  532. [
  533. {"installmentNo": 1, "payStatus": "yjf"},
  534. {"installmentNo": 1, "PayStatus": "yjf"},
  535. {"installmentNo": 1, "financeConfirmAttachments": "secret.pdf"},
  536. {
  537. "installmentNo": 1,
  538. "financeConfirmAttachmentFiles": [{"url": "secret.pdf"}],
  539. },
  540. {"installmentNo": 1, "financeRemark": "confirmed"},
  541. {"installmentNo": 1, "refundAmount": 100},
  542. {"installmentNo": 1, "refundRemark": "refund"},
  543. {"installmentNo": 1, "refundAttachments": "refund.pdf"},
  544. {
  545. "installmentNo": 1,
  546. "refundAttachmentFiles": [{"url": "refund.pdf"}],
  547. },
  548. {
  549. "installmentNo": 1,
  550. "refundFinanceConfirmAttachments": "finance.pdf",
  551. },
  552. {
  553. "installmentNo": 1,
  554. "refundFinanceConfirmAttachmentFiles": [{"url": "finance.pdf"}],
  555. },
  556. {"installmentNo": 1, "refundFinanceRemark": "confirmed"},
  557. ],
  558. )
  559. def test_protocol_create_rejects_nested_finance_and_refund_fields_before_crud(
  560. monkeypatch, payment_detail
  561. ):
  562. from app.api import collection_protocol
  563. called = []
  564. def forbidden_call(*args, **kwargs):
  565. called.append((args, kwargs))
  566. raise AssertionError("protocol CRUD must not be called")
  567. monkeypatch.setattr(
  568. collection_protocol.cp_svc,
  569. "create_collection_protocol",
  570. forbidden_call,
  571. )
  572. body = collection_protocol.ProtocolCreateBody(
  573. paymentModelVersion=1,
  574. paymentDetails=[payment_detail],
  575. )
  576. with pytest.raises(HTTPException) as exc_info:
  577. asyncio.run(
  578. collection_protocol.create_collection_protocol(
  579. body=body,
  580. user={"user_id": "u1", "role": "admin"},
  581. db=None,
  582. )
  583. )
  584. assert exc_info.value.status_code == 400
  585. assert called == []
  586. def test_protocol_update_rejects_snake_case_nested_sensitive_fields_before_crud(
  587. monkeypatch,
  588. ):
  589. from app.api import collection_protocol
  590. called = []
  591. def forbidden_call(*args, **kwargs):
  592. called.append((args, kwargs))
  593. raise AssertionError("protocol CRUD must not be called")
  594. monkeypatch.setattr(
  595. collection_protocol.cp_svc,
  596. "update_collection_protocol",
  597. forbidden_call,
  598. )
  599. body = collection_protocol.ProtocolUpdateBody(
  600. payment_details=[
  601. {
  602. "installment_no": 1,
  603. "finance_confirm_attachment_files": [{"url": "secret.pdf"}],
  604. }
  605. ]
  606. )
  607. with pytest.raises(HTTPException) as exc_info:
  608. asyncio.run(
  609. collection_protocol.update_collection_protocol(
  610. protocol_id="p1",
  611. body=body,
  612. user={"user_id": "u1", "role": "admin"},
  613. db=None,
  614. )
  615. )
  616. assert exc_info.value.status_code == 400
  617. assert called == []
  618. def test_payment_detail_sensitive_field_inventory_matches_crud_write_fields():
  619. from app.api.collection_protocol import _PAYMENT_DETAIL_SENSITIVE_FIELDS
  620. assert _PAYMENT_DETAIL_SENSITIVE_FIELDS == {
  621. "pay_status",
  622. "payStatus",
  623. "finance_confirm_attachments",
  624. "financeConfirmAttachments",
  625. "finance_confirm_attachment_files",
  626. "financeConfirmAttachmentFiles",
  627. "finance_remark",
  628. "financeRemark",
  629. "refund_amount",
  630. "refundAmount",
  631. "refund_remark",
  632. "refundRemark",
  633. "refund_attachments",
  634. "refundAttachments",
  635. "refund_attachment_files",
  636. "refundAttachmentFiles",
  637. "refund_finance_confirm_attachments",
  638. "refundFinanceConfirmAttachments",
  639. "refund_finance_confirm_attachment_files",
  640. "refundFinanceConfirmAttachmentFiles",
  641. "refund_finance_remark",
  642. "refundFinanceRemark",
  643. }