test_message_cleanup.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. from __future__ import annotations
  2. import asyncio
  3. from types import SimpleNamespace
  4. from unittest.mock import AsyncMock
  5. import pytest
  6. class FakeMessage:
  7. def __init__(self, chat_id: int, message_id: int, *, text: str = "") -> None:
  8. self.chat = SimpleNamespace(
  9. id=chat_id,
  10. type=SimpleNamespace(value="supergroup" if chat_id < 0 else "private"),
  11. )
  12. self.id = message_id
  13. self.text = text
  14. self.caption = None
  15. self.delete_calls: list[bool] = []
  16. async def delete(self, *, revoke: bool = True) -> None:
  17. self.delete_calls.append(revoke)
  18. class FakeClient:
  19. def __init__(self) -> None:
  20. self.next_message_id = 100
  21. async def send_message(
  22. self,
  23. chat_id: int,
  24. _text: str,
  25. *,
  26. reply_to_message_id: int | None = None,
  27. ) -> FakeMessage:
  28. self.next_message_id += 1
  29. return FakeMessage(chat_id, self.next_message_id)
  30. @pytest.fixture
  31. def cleanup(app_modules):
  32. module = app_modules.load("wbb.services.message_cleanup")
  33. module._scheduled_messages.clear()
  34. module._cleanup_tasks.clear()
  35. module._active_interactions.clear()
  36. yield module
  37. module._scheduled_messages.clear()
  38. module._cleanup_tasks.clear()
  39. module._active_interactions.clear()
  40. async def test_group_cleanup_revokes_once_for_everyone(cleanup, monkeypatch):
  41. real_sleep = asyncio.sleep
  42. async def immediate_sleep(_delay: int) -> None:
  43. return None
  44. monkeypatch.setattr(cleanup.asyncio, "sleep", immediate_sleep)
  45. message = FakeMessage(-1001, 10, text="/points")
  46. cleanup.schedule_group_cleanup(message)
  47. cleanup.schedule_group_cleanup(message)
  48. await real_sleep(0)
  49. await real_sleep(0)
  50. assert message.delete_calls == [True]
  51. assert (-1001, 10) not in cleanup._scheduled_messages
  52. async def test_private_messages_are_not_scheduled(cleanup):
  53. message = FakeMessage(1001, 11, text="/points")
  54. cleanup.schedule_group_cleanup(message)
  55. assert not cleanup.is_group_cleanup_scheduled(message)
  56. assert message.delete_calls == []
  57. async def test_cleanup_task_is_referenced_until_deletion_finishes(
  58. cleanup, monkeypatch
  59. ):
  60. real_sleep = asyncio.sleep
  61. release = asyncio.Event()
  62. async def blocked_sleep(_delay: int) -> None:
  63. await release.wait()
  64. monkeypatch.setattr(cleanup.asyncio, "sleep", blocked_sleep)
  65. message = FakeMessage(-1001, 17, text="签到")
  66. cleanup.schedule_group_cleanup(message)
  67. await real_sleep(0)
  68. assert len(cleanup._cleanup_tasks) == 1
  69. release.set()
  70. await real_sleep(0)
  71. await real_sleep(0)
  72. assert message.delete_calls == [True]
  73. assert cleanup._cleanup_tasks == set()
  74. async def test_command_context_tracks_only_responses_in_the_same_group(
  75. cleanup, monkeypatch
  76. ):
  77. scheduled: list[FakeMessage] = []
  78. monkeypatch.setattr(
  79. cleanup,
  80. "schedule_group_cleanup",
  81. lambda *messages, **_kwargs: scheduled.extend(messages),
  82. )
  83. client = FakeClient()
  84. cleanup.install_group_interaction_tracking(client)
  85. command = FakeMessage(-1001, 12, text="/points")
  86. assert cleanup.begin_group_message_interaction(command)
  87. group_response = await client.send_message(
  88. -1001, "积分余额", reply_to_message_id=command.id
  89. )
  90. await client.send_message(-1001, "抽奖主消息")
  91. await client.send_message(-1002, "其他群通知", reply_to_message_id=command.id)
  92. await client.send_message(1001, "私聊通知", reply_to_message_id=command.id)
  93. assert scheduled == [command, group_response]
  94. async def test_plain_message_clears_previous_command_context(cleanup, monkeypatch):
  95. scheduled: list[FakeMessage] = []
  96. monkeypatch.setattr(
  97. cleanup,
  98. "schedule_group_cleanup",
  99. lambda *messages, **_kwargs: scheduled.extend(messages),
  100. )
  101. client = FakeClient()
  102. cleanup.install_group_interaction_tracking(client)
  103. command = FakeMessage(-1001, 13, text="/points")
  104. plain_message = FakeMessage(-1001, 14, text="大家好")
  105. assert cleanup.begin_group_message_interaction(command)
  106. assert not cleanup.begin_group_message_interaction(plain_message)
  107. await client.send_message(
  108. -1001, "普通自动回复", reply_to_message_id=plain_message.id
  109. )
  110. assert scheduled == [command]
  111. async def test_checkin_button_is_treated_as_group_interaction(cleanup, monkeypatch):
  112. scheduled: list[FakeMessage] = []
  113. monkeypatch.setattr(
  114. cleanup,
  115. "schedule_group_cleanup",
  116. lambda *messages, **_kwargs: scheduled.extend(messages),
  117. )
  118. message = FakeMessage(-1001, 15, text="签到")
  119. assert cleanup.begin_group_message_interaction(message, menu_texts={"签到"})
  120. assert scheduled == [message]
  121. async def test_callback_only_continues_an_already_ephemeral_menu(
  122. cleanup, monkeypatch
  123. ):
  124. scheduled: list[FakeMessage] = []
  125. monkeypatch.setattr(
  126. cleanup,
  127. "schedule_group_cleanup",
  128. lambda *messages, **_kwargs: scheduled.extend(messages),
  129. )
  130. client = FakeClient()
  131. cleanup.install_group_interaction_tracking(client)
  132. persistent_menu = FakeMessage(-1001, 16)
  133. query = SimpleNamespace(message=persistent_menu)
  134. assert not cleanup.begin_group_callback_interaction(query)
  135. await client.send_message(
  136. -1001, "不应自动删除", reply_to_message_id=persistent_menu.id
  137. )
  138. cleanup._scheduled_messages.add((-1001, 16))
  139. assert cleanup.begin_group_callback_interaction(query)
  140. callback_response = await client.send_message(
  141. -1001, "临时菜单回复", reply_to_message_id=persistent_menu.id
  142. )
  143. assert scheduled == [callback_response]
  144. async def test_group_reply_explicitly_schedules_request_and_response(
  145. cleanup, monkeypatch
  146. ):
  147. scheduled: list[FakeMessage] = []
  148. monkeypatch.setattr(
  149. cleanup,
  150. "schedule_group_cleanup",
  151. lambda *messages, **_kwargs: scheduled.extend(messages),
  152. )
  153. request = FakeMessage(-1001, 18, text="/start")
  154. response = FakeMessage(-1001, 19, text="请私聊我查看详细功能。")
  155. request.reply = AsyncMock(return_value=response)
  156. result = await cleanup.reply_group_interaction(
  157. request,
  158. "请私聊我查看详细功能。",
  159. reply_markup=object(),
  160. )
  161. assert result is response
  162. request.reply.assert_awaited_once()
  163. assert scheduled == [request, response]
  164. async def test_chinese_history_command_tracks_reply_with_configured_ttl(
  165. cleanup, monkeypatch
  166. ):
  167. scheduled: list[tuple[FakeMessage, int]] = []
  168. monkeypatch.setattr(
  169. cleanup,
  170. "schedule_group_cleanup",
  171. lambda *messages, delay_seconds=10: scheduled.extend(
  172. (message, delay_seconds) for message in messages
  173. ),
  174. )
  175. client = FakeClient()
  176. cleanup.install_group_interaction_tracking(client)
  177. command = FakeMessage(-1001, 20, text="流水")
  178. assert cleanup.begin_group_message_interaction(command, delay_seconds=12)
  179. response = await client.send_message(
  180. -1001, "最近积分流水", reply_to_message_id=command.id
  181. )
  182. assert scheduled == [(command, 12), (response, 12)]