test_giveaway_points.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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_draw_rewards_are_idempotent_under_competing_finish_calls(app_modules):
  63. points = app_modules.load("wbb.utils.dbpoints")
  64. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  65. service = app_modules.load("wbb.services.giveaways")
  66. for user_id in (10, 11):
  67. await _seed_points(points, chat_id=-100, user_id=user_id, amount=20)
  68. giveaway = await giveaway_db.create_giveaway(
  69. chat_id=-100,
  70. creator_id=1,
  71. creator_name="Admin",
  72. title="Draw",
  73. description="",
  74. prizes=[{"name": "Winner", "count": 1, "points_reward": 5}],
  75. ends_at=datetime.now(UTC) + timedelta(hours=1),
  76. entry_cost=3,
  77. participation_reward=2,
  78. )
  79. for user_id in (10, 11):
  80. status, _ = await service.join_giveaway(
  81. giveaway["giveaway_id"],
  82. user_id=user_id,
  83. username=None,
  84. first_name=str(user_id),
  85. )
  86. assert status == "ok"
  87. results = await asyncio.gather(
  88. service.finish_and_publish_giveaway(giveaway["giveaway_id"], publish=False),
  89. service.finish_and_publish_giveaway(giveaway["giveaway_id"], publish=False),
  90. )
  91. assert sum(1 for result in results if result[0]) == 1
  92. finished = await giveaway_db.get_giveaway(giveaway["giveaway_id"])
  93. assert finished["status"] == giveaway_db.STATUS_FINISHED
  94. winner_id = int(finished["winners"][0]["user_id"])
  95. balances = {
  96. user_id: (await points.get_point_account(-100, user_id))["balance"]
  97. for user_id in (10, 11)
  98. }
  99. assert balances[winner_id] == 24
  100. assert balances[10 if winner_id == 11 else 11] == 19
  101. async def test_participant_write_failure_creates_compensating_refund(
  102. app_modules, monkeypatch
  103. ):
  104. points = app_modules.load("wbb.utils.dbpoints")
  105. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  106. service = app_modules.load("wbb.services.giveaways")
  107. await _seed_points(points, chat_id=-100, user_id=10, amount=20)
  108. giveaway = await giveaway_db.create_giveaway(
  109. chat_id=-100,
  110. creator_id=1,
  111. creator_name="Admin",
  112. title="Failure",
  113. description="",
  114. prizes=[{"name": "Winner", "count": 1}],
  115. ends_at=datetime.now(UTC) + timedelta(hours=1),
  116. entry_cost=5,
  117. )
  118. original_add_participant = service.add_participant
  119. async def fail_insert(**_kwargs):
  120. raise RuntimeError("simulated participant write failure")
  121. monkeypatch.setattr(service, "add_participant", fail_insert)
  122. with pytest.raises(RuntimeError):
  123. await service.join_giveaway(
  124. giveaway["giveaway_id"],
  125. user_id=10,
  126. username=None,
  127. first_name="Alice",
  128. )
  129. assert (await points.get_point_account(-100, 10))["balance"] == 20
  130. transactions, _ = await points.list_point_transactions(
  131. chat_id=-100, user_id=10, page_size=20
  132. )
  133. assert {item["source"] for item in transactions} >= {
  134. points.SOURCE_GIVEAWAY_ENTRY,
  135. points.SOURCE_GIVEAWAY_REFUND,
  136. }
  137. monkeypatch.setattr(service, "add_participant", original_add_participant)
  138. status, _ = await service.join_giveaway(
  139. giveaway["giveaway_id"],
  140. user_id=10,
  141. username=None,
  142. first_name="Alice",
  143. )
  144. assert status == "ok"
  145. assert (await points.get_point_account(-100, 10))["balance"] == 15
  146. async def test_retry_reuses_uncompensated_entry_debit(app_modules):
  147. points = app_modules.load("wbb.utils.dbpoints")
  148. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  149. service = app_modules.load("wbb.services.giveaways")
  150. await _seed_points(points, chat_id=-100, user_id=10, amount=20)
  151. giveaway = await giveaway_db.create_giveaway(
  152. chat_id=-100,
  153. creator_id=1,
  154. creator_name="Admin",
  155. title="Crash recovery",
  156. description="",
  157. prizes=[{"name": "Winner", "count": 1}],
  158. ends_at=datetime.now(UTC) + timedelta(hours=1),
  159. minimum_points=20,
  160. entry_cost=5,
  161. )
  162. await points.adjust_points(
  163. chat_id=-100,
  164. user_id=10,
  165. delta=-5,
  166. source=points.SOURCE_GIVEAWAY_ENTRY,
  167. idempotency_key=f"giveaway-entry:{giveaway['giveaway_id']}:10",
  168. reference_id=giveaway["giveaway_id"],
  169. reason="simulated crash after debit",
  170. )
  171. status, _ = await service.join_giveaway(
  172. giveaway["giveaway_id"],
  173. user_id=10,
  174. username=None,
  175. first_name="Alice",
  176. )
  177. assert status == "ok"
  178. assert (await points.get_point_account(-100, 10))["balance"] == 15
  179. transactions, _ = await points.list_point_transactions(
  180. chat_id=-100, user_id=10, source=points.SOURCE_GIVEAWAY_ENTRY
  181. )
  182. assert len(transactions) == 1