test_giveaway_points.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. from __future__ import annotations
  2. import asyncio
  3. from datetime import UTC, datetime, timedelta
  4. import pytest
  5. async def _seed_points(points, *, chat_id: int, user_id: int, amount: int) -> None:
  6. await points.adjust_points(
  7. chat_id=chat_id,
  8. user_id=user_id,
  9. delta=amount,
  10. source=points.SOURCE_ADMIN,
  11. idempotency_key=f"seed:{chat_id}:{user_id}",
  12. reason="test seed",
  13. )
  14. async def test_entry_cost_duplicate_click_and_cancel_refund(app_modules):
  15. points = app_modules.load("wbb.utils.dbpoints")
  16. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  17. service = app_modules.load("wbb.services.giveaways")
  18. await _seed_points(points, chat_id=-100, user_id=10, amount=100)
  19. giveaway = await giveaway_db.create_giveaway(
  20. chat_id=-100,
  21. creator_id=1,
  22. creator_name="Admin",
  23. title="Launch",
  24. description="",
  25. prizes=[{"name": "First", "count": 1, "points_reward": 5}],
  26. ends_at=datetime.now(UTC) + timedelta(hours=1),
  27. minimum_points=50,
  28. entry_cost=10,
  29. participation_reward=2,
  30. )
  31. first, _ = await service.join_giveaway(
  32. giveaway["giveaway_id"],
  33. user_id=10,
  34. username="alice",
  35. first_name="Alice",
  36. display_name="Alice Example",
  37. )
  38. duplicate, _ = await service.join_giveaway(
  39. giveaway["giveaway_id"],
  40. user_id=10,
  41. username="alice",
  42. first_name="Alice",
  43. display_name="Alice Example",
  44. )
  45. assert (first, duplicate) == ("ok", "duplicate")
  46. account = await points.get_point_account(-100, 10)
  47. assert account["balance"] == 90
  48. assert account["display_name"] == "Alice Example"
  49. participant = await giveaway_db.get_participant(giveaway["giveaway_id"], 10)
  50. assert participant["display_name"] == "Alice Example"
  51. assert "Alice Example" in service._participant_mention(participant)
  52. assert "@alice" not in service._participant_mention(participant)
  53. ok, _, _ = await service.cancel_and_refund_giveaway(
  54. giveaway["giveaway_id"], chat_id=-100, publish=False
  55. )
  56. again, _, _ = await service.cancel_and_refund_giveaway(
  57. giveaway["giveaway_id"], chat_id=-100, publish=False
  58. )
  59. assert ok is True
  60. assert again is False
  61. assert (await points.get_point_account(-100, 10))["balance"] == 100
  62. async def test_registration_window_blocks_early_entry_and_refreshes_count(app_modules):
  63. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  64. service = app_modules.load("wbb.services.giveaways")
  65. now = datetime.now(UTC)
  66. future = await giveaway_db.create_giveaway(
  67. chat_id=-100,
  68. creator_id=1,
  69. creator_name="Admin",
  70. title="Scheduled",
  71. description="",
  72. prizes=[{"name": "Winner", "count": 1}],
  73. starts_at=now + timedelta(hours=1),
  74. ends_at=now + timedelta(hours=2),
  75. )
  76. status, _ = await service.join_giveaway(
  77. future["giveaway_id"],
  78. user_id=10,
  79. username=None,
  80. first_name="Alice",
  81. )
  82. assert status == "not_started"
  83. assert await giveaway_db.count_participants(future["giveaway_id"]) == 0
  84. assert await giveaway_db.list_unannounced_started_giveaways() == []
  85. await giveaway_db.giveawaysdb.update_one(
  86. {"giveaway_id": future["giveaway_id"]},
  87. {"$set": {"starts_at": now - timedelta(minutes=1)}},
  88. )
  89. started = await giveaway_db.list_unannounced_started_giveaways()
  90. assert [item["giveaway_id"] for item in started] == [future["giveaway_id"]]
  91. await giveaway_db.mark_giveaway_start_announced(future["giveaway_id"])
  92. assert await giveaway_db.list_unannounced_started_giveaways() == []
  93. current = await giveaway_db.create_giveaway(
  94. chat_id=-100,
  95. creator_id=1,
  96. creator_name="Admin",
  97. title="Open",
  98. description="",
  99. prizes=[{"name": "Winner", "count": 1}],
  100. starts_at=now - timedelta(minutes=1),
  101. ends_at=now + timedelta(hours=1),
  102. )
  103. await giveaway_db.attach_giveaway_message(current["giveaway_id"], -100, 99)
  104. status, _ = await service.join_giveaway(
  105. current["giveaway_id"],
  106. user_id=10,
  107. username="alice",
  108. first_name="Alice",
  109. )
  110. assert status == "ok"
  111. assert len(app_modules.app.edited_messages) == 1
  112. assert app_modules.app.edited_messages[0][:2] == (-100, 99)
  113. assert "<b>参与人数:</b>1" in app_modules.app.edited_messages[0][2]
  114. async def test_registration_start_must_precede_draw_time(app_modules):
  115. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  116. now = datetime.now(UTC)
  117. with pytest.raises(ValueError, match="报名开始时间必须早于开奖时间"):
  118. await giveaway_db.create_giveaway(
  119. chat_id=-100,
  120. creator_id=1,
  121. creator_name="Admin",
  122. title="Invalid schedule",
  123. description="",
  124. prizes=[{"name": "Winner", "count": 1}],
  125. starts_at=now + timedelta(hours=2),
  126. ends_at=now + timedelta(hours=1),
  127. )
  128. async def test_draw_rewards_are_idempotent_under_competing_finish_calls(app_modules):
  129. points = app_modules.load("wbb.utils.dbpoints")
  130. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  131. service = app_modules.load("wbb.services.giveaways")
  132. for user_id in (10, 11):
  133. await _seed_points(points, chat_id=-100, user_id=user_id, amount=20)
  134. giveaway = await giveaway_db.create_giveaway(
  135. chat_id=-100,
  136. creator_id=1,
  137. creator_name="Admin",
  138. title="Draw",
  139. description="",
  140. prizes=[{"name": "Winner", "count": 1, "points_reward": 5}],
  141. ends_at=datetime.now(UTC) + timedelta(hours=1),
  142. entry_cost=3,
  143. participation_reward=2,
  144. )
  145. for user_id in (10, 11):
  146. status, _ = await service.join_giveaway(
  147. giveaway["giveaway_id"],
  148. user_id=user_id,
  149. username=None,
  150. first_name=str(user_id),
  151. )
  152. assert status == "ok"
  153. results = await asyncio.gather(
  154. service.finish_and_publish_giveaway(giveaway["giveaway_id"], publish=False),
  155. service.finish_and_publish_giveaway(giveaway["giveaway_id"], publish=False),
  156. )
  157. assert sum(1 for result in results if result[0]) == 1
  158. finished = await giveaway_db.get_giveaway(giveaway["giveaway_id"])
  159. assert finished["status"] == giveaway_db.STATUS_FINISHED
  160. winner_id = int(finished["winners"][0]["user_id"])
  161. balances = {
  162. user_id: (await points.get_point_account(-100, user_id))["balance"]
  163. for user_id in (10, 11)
  164. }
  165. assert balances[winner_id] == 24
  166. assert balances[10 if winner_id == 11 else 11] == 19
  167. async def test_participant_write_failure_creates_compensating_refund(
  168. app_modules, monkeypatch
  169. ):
  170. points = app_modules.load("wbb.utils.dbpoints")
  171. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  172. service = app_modules.load("wbb.services.giveaways")
  173. await _seed_points(points, chat_id=-100, user_id=10, amount=20)
  174. giveaway = await giveaway_db.create_giveaway(
  175. chat_id=-100,
  176. creator_id=1,
  177. creator_name="Admin",
  178. title="Failure",
  179. description="",
  180. prizes=[{"name": "Winner", "count": 1}],
  181. ends_at=datetime.now(UTC) + timedelta(hours=1),
  182. entry_cost=5,
  183. )
  184. original_add_participant = service.add_participant
  185. async def fail_insert(**_kwargs):
  186. raise RuntimeError("simulated participant write failure")
  187. monkeypatch.setattr(service, "add_participant", fail_insert)
  188. with pytest.raises(RuntimeError):
  189. await service.join_giveaway(
  190. giveaway["giveaway_id"],
  191. user_id=10,
  192. username=None,
  193. first_name="Alice",
  194. )
  195. assert (await points.get_point_account(-100, 10))["balance"] == 20
  196. transactions, _ = await points.list_point_transactions(
  197. chat_id=-100, user_id=10, page_size=20
  198. )
  199. assert {item["source"] for item in transactions} >= {
  200. points.SOURCE_GIVEAWAY_ENTRY,
  201. points.SOURCE_GIVEAWAY_REFUND,
  202. }
  203. monkeypatch.setattr(service, "add_participant", original_add_participant)
  204. status, _ = await service.join_giveaway(
  205. giveaway["giveaway_id"],
  206. user_id=10,
  207. username=None,
  208. first_name="Alice",
  209. )
  210. assert status == "ok"
  211. assert (await points.get_point_account(-100, 10))["balance"] == 15
  212. async def test_retry_reuses_uncompensated_entry_debit(app_modules):
  213. points = app_modules.load("wbb.utils.dbpoints")
  214. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  215. service = app_modules.load("wbb.services.giveaways")
  216. await _seed_points(points, chat_id=-100, user_id=10, amount=20)
  217. giveaway = await giveaway_db.create_giveaway(
  218. chat_id=-100,
  219. creator_id=1,
  220. creator_name="Admin",
  221. title="Crash recovery",
  222. description="",
  223. prizes=[{"name": "Winner", "count": 1}],
  224. ends_at=datetime.now(UTC) + timedelta(hours=1),
  225. minimum_points=20,
  226. entry_cost=5,
  227. )
  228. await points.adjust_points(
  229. chat_id=-100,
  230. user_id=10,
  231. delta=-5,
  232. source=points.SOURCE_GIVEAWAY_ENTRY,
  233. idempotency_key=f"giveaway-entry:{giveaway['giveaway_id']}:10",
  234. reference_id=giveaway["giveaway_id"],
  235. reason="simulated crash after debit",
  236. )
  237. status, _ = await service.join_giveaway(
  238. giveaway["giveaway_id"],
  239. user_id=10,
  240. username=None,
  241. first_name="Alice",
  242. )
  243. assert status == "ok"
  244. assert (await points.get_point_account(-100, 10))["balance"] == 15
  245. transactions, _ = await points.list_point_transactions(
  246. chat_id=-100, user_id=10, source=points.SOURCE_GIVEAWAY_ENTRY
  247. )
  248. assert len(transactions) == 1