test_collection_protocol_conventions.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. """
  2. Tests for collection_protocol API/CRUD convention compliance.
  3. Per AGENTS.md engineering standards:
  4. 1. _get_tenant_no() MUST NOT silently fall back to hardcoded "zkxw"
  5. — must raise PermissionError when user_id missing or SysUser not found.
  6. 2. All list queries SHALL sort by order_num ASC, create_time DESC
  7. (unless a business-specific sort is justified in the design doc).
  8. 3. list_audits, list_receipt_confirm_details, list_refund_confirm_details
  9. currently only sort by create_time DESC — missing order_num ASC prefix.
  10. """
  11. import unittest
  12. from unittest.mock import MagicMock, patch, call
  13. # ---------------------------------------------------------------------------
  14. # H1: _get_tenant_no must raise PermissionError (not fall back to "zkxw")
  15. # ---------------------------------------------------------------------------
  16. class TestCollectionProtocolGetTenantNo(unittest.TestCase):
  17. """collection_protocol._get_tenant_no should match basic_info pattern."""
  18. def test_raises_on_missing_user_id(self):
  19. """When user dict has no user_id, must raise PermissionError."""
  20. from app.api.collection_protocol import _get_tenant_no
  21. mock_db = MagicMock()
  22. user = {} # no user_id key
  23. with self.assertRaises(PermissionError):
  24. _get_tenant_no(mock_db, user)
  25. def test_raises_on_none_user_id(self):
  26. """When user_id is None, must raise PermissionError."""
  27. from app.api.collection_protocol import _get_tenant_no
  28. mock_db = MagicMock()
  29. user = {"user_id": None}
  30. with self.assertRaises(PermissionError):
  31. _get_tenant_no(mock_db, user)
  32. def test_raises_on_empty_user_id(self):
  33. """When user_id is empty string, must raise PermissionError."""
  34. from app.api.collection_protocol import _get_tenant_no
  35. mock_db = MagicMock()
  36. user = {"user_id": ""}
  37. with self.assertRaises(PermissionError):
  38. _get_tenant_no(mock_db, user)
  39. def test_raises_on_missing_sys_user(self):
  40. """When SysUser record not found in DB, must raise PermissionError."""
  41. from app.api.collection_protocol import _get_tenant_no
  42. mock_db = MagicMock()
  43. mock_query = MagicMock()
  44. mock_db.query.return_value = mock_query
  45. mock_query.filter.return_value = mock_query
  46. mock_query.first.return_value = None # SysUser not found
  47. user = {"user_id": "some-id"}
  48. with self.assertRaises(PermissionError):
  49. _get_tenant_no(mock_db, user)
  50. def test_raises_on_sys_user_no_tenant(self):
  51. """When SysUser exists but tenant_no is None, must raise PermissionError."""
  52. from app.api.collection_protocol import _get_tenant_no
  53. mock_db = MagicMock()
  54. mock_query = MagicMock()
  55. mock_db.query.return_value = mock_query
  56. mock_query.filter.return_value = mock_query
  57. mock_user = MagicMock()
  58. mock_user.tenant_no = None
  59. mock_query.first.return_value = mock_user
  60. user = {"user_id": "some-id"}
  61. with self.assertRaises(PermissionError):
  62. _get_tenant_no(mock_db, user)
  63. def test_returns_tenant_no_on_valid_user(self):
  64. """When SysUser has valid tenant_no, should return it."""
  65. from app.api.collection_protocol import _get_tenant_no
  66. mock_db = MagicMock()
  67. mock_query = MagicMock()
  68. mock_db.query.return_value = mock_query
  69. mock_query.filter.return_value = mock_query
  70. mock_user = MagicMock()
  71. mock_user.tenant_no = "tenant-ABC"
  72. mock_query.first.return_value = mock_user
  73. result = _get_tenant_no(mock_db, {"user_id": "user-1"})
  74. self.assertEqual(result, "tenant-ABC")
  75. def test_does_not_return_hardcoded_zkxw(self):
  76. """Must never return the hardcoded fallback 'zkxw'."""
  77. from app.api.collection_protocol import _get_tenant_no
  78. mock_db = MagicMock()
  79. mock_query = MagicMock()
  80. mock_db.query.return_value = mock_query
  81. mock_query.filter.return_value = mock_query
  82. mock_query.first.return_value = None
  83. user = {"user_id": "missing-user"}
  84. with self.assertRaises(PermissionError) as ctx:
  85. _get_tenant_no(mock_db, user)
  86. # Ensure it raised, not returned "zkxw"
  87. self.assertNotIsInstance(ctx.exception, type(None))
  88. # ---------------------------------------------------------------------------
  89. # M1: Sort order — list_audits should include order_num ASC
  90. # ---------------------------------------------------------------------------
  91. class TestListAuditsSortOrder(unittest.TestCase):
  92. """list_audits should sort by order_num ASC, create_time DESC."""
  93. @patch("app.crud.collection_protocol.BizProtocolAudit")
  94. @patch("app.crud.collection_protocol.BizCollectionProtocol")
  95. def test_list_audits_includes_order_num_in_sort(self, MockProtocol, MockAudit):
  96. """order_by should include order_num ASC before create_time DESC."""
  97. from app.crud.collection_protocol import list_audits
  98. mock_db = MagicMock()
  99. mock_query = MagicMock()
  100. mock_query.join.return_value = mock_query
  101. mock_query.filter.return_value = mock_query
  102. mock_query.order_by.return_value = mock_query
  103. mock_query.with_entities.return_value = mock_query
  104. mock_query.scalar.return_value = 0
  105. mock_query.offset.return_value = mock_query
  106. mock_query.limit.return_value = mock_query
  107. mock_query.all.return_value = []
  108. mock_db.query.return_value = mock_query
  109. list_audits(mock_db, tenant_no="t1")
  110. # Check that order_by was called at least once for the data query
  111. # The sort call should include order_num ASC
  112. order_by_calls = mock_query.order_by.call_args_list
  113. self.assertGreaterEqual(len(order_by_calls), 1, "order_by should be called for data query")
  114. # The data query's order_by should contain order_num.asc()
  115. # We check the last order_by call (the data query, not the count query)
  116. last_sort_call = order_by_calls[-1]
  117. sort_args = last_sort_call[0] # positional args
  118. # Should have at least 2 args: order_num ASC, create_time DESC
  119. self.assertGreaterEqual(
  120. len(sort_args), 2,
  121. f"order_by should have at least 2 args (order_num ASC, create_time DESC), got: {sort_args}"
  122. )
  123. # ---------------------------------------------------------------------------
  124. # M1: Sort order — list_receipt_confirm_details should include order_num ASC
  125. # ---------------------------------------------------------------------------
  126. class TestListReceiptConfirmSortOrder(unittest.TestCase):
  127. """list_receipt_confirm_details should sort by order_num ASC, create_time DESC."""
  128. @patch("app.crud.collection_protocol.BizProtocolPaymentDetail")
  129. @patch("app.crud.collection_protocol.BizProtocolPayment")
  130. @patch("app.crud.collection_protocol.BizCollectionProtocol")
  131. def test_receipt_confirm_includes_order_num_in_sort(self, MockProtocol, MockPayment, MockDetail):
  132. """order_by should include order_num ASC before create_time DESC."""
  133. from app.crud.collection_protocol import list_receipt_confirm_details
  134. mock_db = MagicMock()
  135. mock_query = MagicMock()
  136. mock_query.join.return_value = mock_query
  137. mock_query.filter.return_value = mock_query
  138. mock_query.order_by.return_value = mock_query
  139. mock_query.with_entities.return_value = mock_query
  140. mock_query.scalar.return_value = 0
  141. mock_query.offset.return_value = mock_query
  142. mock_query.limit.return_value = mock_query
  143. mock_query.all.return_value = []
  144. mock_db.query.return_value = mock_query
  145. list_receipt_confirm_details(mock_db, tenant_no="t1")
  146. order_by_calls = mock_query.order_by.call_args_list
  147. self.assertGreaterEqual(len(order_by_calls), 1)
  148. last_sort_call = order_by_calls[-1]
  149. sort_args = last_sort_call[0]
  150. self.assertGreaterEqual(
  151. len(sort_args), 2,
  152. f"order_by should have at least 2 args, got: {sort_args}"
  153. )
  154. class TestListReceiptConfirmedSortOrder(unittest.TestCase):
  155. """list_receipt_confirmed_details should sort by update_time DESC."""
  156. @patch("app.crud.collection_protocol.BizProtocolPaymentDetail")
  157. @patch("app.crud.collection_protocol.BizProtocolPayment")
  158. @patch("app.crud.collection_protocol.BizCollectionProtocol")
  159. def test_receipt_confirmed_sorts_by_update_time_desc(self, MockProtocol, MockPayment, MockDetail):
  160. from app.crud.collection_protocol import list_receipt_confirmed_details
  161. mock_db = MagicMock()
  162. mock_query = MagicMock()
  163. mock_query.join.return_value = mock_query
  164. mock_query.filter.return_value = mock_query
  165. mock_query.order_by.return_value = mock_query
  166. mock_query.with_entities.return_value = mock_query
  167. mock_query.scalar.return_value = 0
  168. mock_query.offset.return_value = mock_query
  169. mock_query.limit.return_value = mock_query
  170. mock_query.all.return_value = []
  171. mock_db.query.return_value = mock_query
  172. list_receipt_confirmed_details(mock_db, tenant_no="t1")
  173. order_by_calls = mock_query.order_by.call_args_list
  174. self.assertGreaterEqual(len(order_by_calls), 1)
  175. class TestListRefundConfirmedSortOrder(unittest.TestCase):
  176. """list_refund_confirmed_details should sort by update_time DESC."""
  177. @patch("app.crud.collection_protocol.BizProtocolPaymentDetail")
  178. @patch("app.crud.collection_protocol.BizProtocolPayment")
  179. @patch("app.crud.collection_protocol.BizCollectionProtocol")
  180. def test_refund_confirmed_sorts_by_update_time_desc(self, MockProtocol, MockPayment, MockDetail):
  181. from app.crud.collection_protocol import list_refund_confirmed_details
  182. mock_db = MagicMock()
  183. mock_query = MagicMock()
  184. mock_query.join.return_value = mock_query
  185. mock_query.filter.return_value = mock_query
  186. mock_query.order_by.return_value = mock_query
  187. mock_query.with_entities.return_value = mock_query
  188. mock_query.scalar.return_value = 0
  189. mock_query.offset.return_value = mock_query
  190. mock_query.limit.return_value = mock_query
  191. mock_query.all.return_value = []
  192. mock_db.query.return_value = mock_query
  193. list_refund_confirmed_details(mock_db, tenant_no="t1")
  194. order_by_calls = mock_query.order_by.call_args_list
  195. self.assertGreaterEqual(len(order_by_calls), 1)
  196. # ---------------------------------------------------------------------------
  197. # M1: Sort order — list_refund_confirm_details should include order_num ASC
  198. # ---------------------------------------------------------------------------
  199. class TestListRefundConfirmSortOrder(unittest.TestCase):
  200. """list_refund_confirm_details should sort by order_num ASC, create_time DESC."""
  201. @patch("app.crud.collection_protocol.BizProtocolPaymentDetail")
  202. @patch("app.crud.collection_protocol.BizProtocolPayment")
  203. @patch("app.crud.collection_protocol.BizCollectionProtocol")
  204. def test_refund_confirm_includes_order_num_in_sort(self, MockProtocol, MockPayment, MockDetail):
  205. """order_by should include order_num ASC before create_time DESC."""
  206. from app.crud.collection_protocol import list_refund_confirm_details
  207. mock_db = MagicMock()
  208. mock_query = MagicMock()
  209. mock_query.join.return_value = mock_query
  210. mock_query.filter.return_value = mock_query
  211. mock_query.order_by.return_value = mock_query
  212. mock_query.with_entities.return_value = mock_query
  213. mock_query.scalar.return_value = 0
  214. mock_query.offset.return_value = mock_query
  215. mock_query.limit.return_value = mock_query
  216. mock_query.all.return_value = []
  217. mock_db.query.return_value = mock_query
  218. list_refund_confirm_details(mock_db, tenant_no="t1")
  219. order_by_calls = mock_query.order_by.call_args_list
  220. self.assertGreaterEqual(len(order_by_calls), 1)
  221. last_sort_call = order_by_calls[-1]
  222. sort_args = last_sort_call[0]
  223. self.assertGreaterEqual(
  224. len(sort_args), 2,
  225. f"order_by should have at least 2 args, got: {sort_args}"
  226. )
  227. class TestNormalizeYyyymmddDate(unittest.TestCase):
  228. def test_compact_and_dashed(self):
  229. from app.crud.collection_protocol import _normalize_yyyymmdd_date
  230. self.assertEqual(_normalize_yyyymmdd_date("20260506"), "20260506")
  231. self.assertEqual(_normalize_yyyymmdd_date("2026-05-06"), "20260506")
  232. self.assertIsNone(_normalize_yyyymmdd_date(""))
  233. self.assertIsNone(_normalize_yyyymmdd_date(None))
  234. if __name__ == "__main__":
  235. unittest.main()