| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- from __future__ import annotations
- import asyncio
- from datetime import UTC, datetime, timedelta
- import pytest
- async def _seed_points(points, *, chat_id: int, user_id: int, amount: int) -> None:
- await points.adjust_points(
- chat_id=chat_id,
- user_id=user_id,
- delta=amount,
- source=points.SOURCE_ADMIN,
- idempotency_key=f"seed:{chat_id}:{user_id}",
- reason="test seed",
- )
- async def test_entry_cost_duplicate_click_and_cancel_refund(app_modules):
- points = app_modules.load("wbb.utils.dbpoints")
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- service = app_modules.load("wbb.services.giveaways")
- await _seed_points(points, chat_id=-100, user_id=10, amount=100)
- giveaway = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Launch",
- description="",
- prizes=[{"name": "First", "count": 1, "points_reward": 5}],
- ends_at=datetime.now(UTC) + timedelta(hours=1),
- minimum_points=50,
- entry_cost=10,
- participation_reward=2,
- )
- first, _ = await service.join_giveaway(
- giveaway["giveaway_id"],
- user_id=10,
- username="alice",
- first_name="Alice",
- display_name="Alice Example",
- )
- duplicate, _ = await service.join_giveaway(
- giveaway["giveaway_id"],
- user_id=10,
- username="alice",
- first_name="Alice",
- display_name="Alice Example",
- )
- assert (first, duplicate) == ("ok", "duplicate")
- account = await points.get_point_account(-100, 10)
- assert account["balance"] == 90
- assert account["display_name"] == "Alice Example"
- participant = await giveaway_db.get_participant(giveaway["giveaway_id"], 10)
- assert participant["display_name"] == "Alice Example"
- assert "Alice Example" in service._participant_mention(participant)
- assert "@alice" not in service._participant_mention(participant)
- ok, _, _ = await service.cancel_and_refund_giveaway(
- giveaway["giveaway_id"], chat_id=-100, publish=False
- )
- again, _, _ = await service.cancel_and_refund_giveaway(
- giveaway["giveaway_id"], chat_id=-100, publish=False
- )
- assert ok is True
- assert again is False
- assert (await points.get_point_account(-100, 10))["balance"] == 100
- async def test_registration_window_blocks_early_entry_and_refreshes_count(app_modules):
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- service = app_modules.load("wbb.services.giveaways")
- now = datetime.now(UTC)
- future = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Scheduled",
- description="",
- prizes=[{"name": "Winner", "count": 1}],
- starts_at=now + timedelta(hours=1),
- ends_at=now + timedelta(hours=2),
- )
- status, _ = await service.join_giveaway(
- future["giveaway_id"],
- user_id=10,
- username=None,
- first_name="Alice",
- )
- assert status == "not_started"
- assert await giveaway_db.count_participants(future["giveaway_id"]) == 0
- assert await giveaway_db.list_unannounced_started_giveaways() == []
- await giveaway_db.giveawaysdb.update_one(
- {"giveaway_id": future["giveaway_id"]},
- {"$set": {"starts_at": now - timedelta(minutes=1)}},
- )
- started = await giveaway_db.list_unannounced_started_giveaways()
- assert [item["giveaway_id"] for item in started] == [future["giveaway_id"]]
- await giveaway_db.mark_giveaway_start_announced(future["giveaway_id"])
- assert await giveaway_db.list_unannounced_started_giveaways() == []
- current = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Open",
- description="",
- prizes=[{"name": "Winner", "count": 1}],
- starts_at=now - timedelta(minutes=1),
- ends_at=now + timedelta(hours=1),
- )
- await giveaway_db.attach_giveaway_message(current["giveaway_id"], -100, 99)
- status, _ = await service.join_giveaway(
- current["giveaway_id"],
- user_id=10,
- username="alice",
- first_name="Alice",
- )
- assert status == "ok"
- assert len(app_modules.app.edited_messages) == 1
- assert app_modules.app.edited_messages[0][:2] == (-100, 99)
- assert "<b>参与人数:</b>1" in app_modules.app.edited_messages[0][2]
- async def test_registration_start_must_precede_draw_time(app_modules):
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- now = datetime.now(UTC)
- with pytest.raises(ValueError, match="报名开始时间必须早于开奖时间"):
- await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Invalid schedule",
- description="",
- prizes=[{"name": "Winner", "count": 1}],
- starts_at=now + timedelta(hours=2),
- ends_at=now + timedelta(hours=1),
- )
- async def test_running_giveaway_update_refreshes_message_and_rejects_stale_write(
- app_modules,
- ):
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- service = app_modules.load("wbb.services.giveaways")
- now = datetime.now(UTC)
- giveaway = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Original title",
- description="Original description",
- prizes=[{"name": "Winner", "count": 1, "points_reward": 5}],
- starts_at=now + timedelta(hours=1),
- ends_at=now + timedelta(hours=2),
- minimum_points=20,
- entry_cost=5,
- participation_reward=2,
- )
- await giveaway_db.attach_giveaway_message(giveaway["giveaway_id"], -100, 99)
- current = await giveaway_db.get_giveaway(giveaway["giveaway_id"])
- new_starts_at = now + timedelta(hours=2)
- new_ends_at = now + timedelta(hours=3)
- updated = await service.update_and_refresh_giveaway(
- giveaway["giveaway_id"],
- title="Updated title",
- description="Valid for one month",
- starts_at=new_starts_at,
- ends_at=new_ends_at,
- expected_updated_at=current["updated_at"],
- )
- assert updated["title"] == "Updated title"
- assert updated["description"] == "Valid for one month"
- assert int(giveaway_db.as_utc(updated["starts_at"]).timestamp() * 1000) == int(
- new_starts_at.timestamp() * 1000
- )
- assert int(giveaway_db.as_utc(updated["ends_at"]).timestamp() * 1000) == int(
- new_ends_at.timestamp() * 1000
- )
- assert updated["prizes"] == giveaway["prizes"]
- assert updated["minimum_points"] == 20
- assert updated["entry_cost"] == 5
- assert updated["participation_reward"] == 2
- assert app_modules.app.edited_messages[-1][:2] == (-100, 99)
- assert "Updated title" in app_modules.app.edited_messages[-1][2]
- assert "Valid for one month" in app_modules.app.edited_messages[-1][2]
- with pytest.raises(service.GiveawayServiceError) as error:
- await service.update_and_refresh_giveaway(
- giveaway["giveaway_id"],
- title="Stale title",
- description="Stale description",
- starts_at=new_starts_at,
- ends_at=new_ends_at,
- expected_updated_at=current["updated_at"],
- )
- assert error.value.code == "giveaway_stale"
- async def test_running_giveaway_cannot_move_registration_start_after_it_begins(
- app_modules,
- ):
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- service = app_modules.load("wbb.services.giveaways")
- now = datetime.now(UTC)
- giveaway = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Open giveaway",
- description="",
- prizes=[{"name": "Winner", "count": 1}],
- starts_at=now - timedelta(minutes=1),
- ends_at=now + timedelta(hours=1),
- )
- with pytest.raises(service.GiveawayServiceError) as error:
- await service.update_and_refresh_giveaway(
- giveaway["giveaway_id"],
- title="Open giveaway",
- description="Updated",
- starts_at=now + timedelta(minutes=1),
- ends_at=now + timedelta(hours=2),
- expected_updated_at=giveaway["updated_at"],
- )
- assert error.value.code == "giveaway_registration_started"
- async def test_draw_rewards_are_idempotent_under_competing_finish_calls(app_modules):
- points = app_modules.load("wbb.utils.dbpoints")
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- service = app_modules.load("wbb.services.giveaways")
- for user_id in (10, 11):
- await _seed_points(points, chat_id=-100, user_id=user_id, amount=20)
- giveaway = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Draw",
- description="",
- prizes=[{"name": "Winner", "count": 1, "points_reward": 5}],
- ends_at=datetime.now(UTC) + timedelta(hours=1),
- entry_cost=3,
- participation_reward=2,
- )
- for user_id in (10, 11):
- status, _ = await service.join_giveaway(
- giveaway["giveaway_id"],
- user_id=user_id,
- username=None,
- first_name=str(user_id),
- )
- assert status == "ok"
- results = await asyncio.gather(
- service.finish_and_publish_giveaway(giveaway["giveaway_id"], publish=False),
- service.finish_and_publish_giveaway(giveaway["giveaway_id"], publish=False),
- )
- assert sum(1 for result in results if result[0]) == 1
- finished = await giveaway_db.get_giveaway(giveaway["giveaway_id"])
- assert finished["status"] == giveaway_db.STATUS_FINISHED
- winner_id = int(finished["winners"][0]["user_id"])
- balances = {
- user_id: (await points.get_point_account(-100, user_id))["balance"]
- for user_id in (10, 11)
- }
- assert balances[winner_id] == 24
- assert balances[10 if winner_id == 11 else 11] == 19
- async def test_draw_pins_result_and_deletes_original_giveaway(app_modules):
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- service = app_modules.load("wbb.services.giveaways")
- giveaway = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Pinned draw",
- description="",
- prizes=[{"name": "Winner", "count": 1}],
- ends_at=datetime.now(UTC) + timedelta(hours=1),
- )
- await giveaway_db.attach_giveaway_message(
- giveaway["giveaway_id"], -100, 99
- )
- await giveaway_db.add_participant(
- giveaway_id=giveaway["giveaway_id"],
- user_id=10,
- username="alice",
- first_name="Alice",
- )
- ok, _, _ = await service.finish_and_publish_giveaway(
- giveaway["giveaway_id"]
- )
- assert ok is True
- assert app_modules.app.pinned_messages == [(-100, 1)]
- assert app_modules.app.deleted_messages == [(-100, 99)]
- assert app_modules.app.unpinned_messages == []
- assert "抽奖结果" in app_modules.app.sent_messages[0][1]
- async def test_participant_write_failure_creates_compensating_refund(
- app_modules, monkeypatch
- ):
- points = app_modules.load("wbb.utils.dbpoints")
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- service = app_modules.load("wbb.services.giveaways")
- await _seed_points(points, chat_id=-100, user_id=10, amount=20)
- giveaway = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Failure",
- description="",
- prizes=[{"name": "Winner", "count": 1}],
- ends_at=datetime.now(UTC) + timedelta(hours=1),
- entry_cost=5,
- )
- original_add_participant = service.add_participant
- async def fail_insert(**_kwargs):
- raise RuntimeError("simulated participant write failure")
- monkeypatch.setattr(service, "add_participant", fail_insert)
- with pytest.raises(RuntimeError):
- await service.join_giveaway(
- giveaway["giveaway_id"],
- user_id=10,
- username=None,
- first_name="Alice",
- )
- assert (await points.get_point_account(-100, 10))["balance"] == 20
- transactions, _ = await points.list_point_transactions(
- chat_id=-100, user_id=10, page_size=20
- )
- assert {item["source"] for item in transactions} >= {
- points.SOURCE_GIVEAWAY_ENTRY,
- points.SOURCE_GIVEAWAY_REFUND,
- }
- monkeypatch.setattr(service, "add_participant", original_add_participant)
- status, _ = await service.join_giveaway(
- giveaway["giveaway_id"],
- user_id=10,
- username=None,
- first_name="Alice",
- )
- assert status == "ok"
- assert (await points.get_point_account(-100, 10))["balance"] == 15
- async def test_retry_reuses_uncompensated_entry_debit(app_modules):
- points = app_modules.load("wbb.utils.dbpoints")
- giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
- service = app_modules.load("wbb.services.giveaways")
- await _seed_points(points, chat_id=-100, user_id=10, amount=20)
- giveaway = await giveaway_db.create_giveaway(
- chat_id=-100,
- creator_id=1,
- creator_name="Admin",
- title="Crash recovery",
- description="",
- prizes=[{"name": "Winner", "count": 1}],
- ends_at=datetime.now(UTC) + timedelta(hours=1),
- minimum_points=20,
- entry_cost=5,
- )
- await points.adjust_points(
- chat_id=-100,
- user_id=10,
- delta=-5,
- source=points.SOURCE_GIVEAWAY_ENTRY,
- idempotency_key=f"giveaway-entry:{giveaway['giveaway_id']}:10",
- reference_id=giveaway["giveaway_id"],
- reason="simulated crash after debit",
- )
- status, _ = await service.join_giveaway(
- giveaway["giveaway_id"],
- user_id=10,
- username=None,
- first_name="Alice",
- )
- assert status == "ok"
- assert (await points.get_point_account(-100, 10))["balance"] == 15
- transactions, _ = await points.list_point_transactions(
- chat_id=-100, user_id=10, source=points.SOURCE_GIVEAWAY_ENTRY
- )
- assert len(transactions) == 1
|