test_chat_management.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. from __future__ import annotations
  2. from unittest.mock import AsyncMock
  3. import pytest
  4. from pyrogram.enums import ChatMemberStatus
  5. from pyrogram.errors import ChatNotModified
  6. def chat_overview() -> dict[str, object]:
  7. return {
  8. "chat_id": -100,
  9. "title": "旧标题",
  10. "description": "旧描述",
  11. "type": "supergroup",
  12. "accessible": True,
  13. "bot_status": "administrator",
  14. "bot_privileges": ["can_change_info"],
  15. "permissions": {},
  16. }
  17. async def test_remove_unavailable_chat_only_deletes_management_index(app_modules):
  18. service = app_modules.load("wbb.services.chat_management")
  19. await app_modules.wbb.db.managed_chats.insert_one(
  20. {
  21. "bot_id": "primary",
  22. "chat_id": -100,
  23. "title": "机器人已退出的群",
  24. "accessible": False,
  25. }
  26. )
  27. await app_modules.wbb.db.managed_chat_settings.insert_one(
  28. {"bot_id": "primary", "chat_id": -100, "welcome_enabled": True}
  29. )
  30. assert await service.remove_unavailable_chat(-100) == {
  31. "chat_id": "-100",
  32. "removed": True,
  33. }
  34. assert await app_modules.wbb.db.managed_chats.find_one({"chat_id": -100}) is None
  35. assert (
  36. await app_modules.wbb.db.managed_chat_settings.find_one({"chat_id": -100})
  37. is not None
  38. )
  39. async def test_remove_unavailable_chat_rejects_accessible_chat(app_modules):
  40. service = app_modules.load("wbb.services.chat_management")
  41. await app_modules.wbb.db.managed_chats.insert_one(
  42. {
  43. "bot_id": "primary",
  44. "chat_id": -100,
  45. "title": "仍可管理的群",
  46. "accessible": True,
  47. }
  48. )
  49. with pytest.raises(service.ChatManagementError) as caught:
  50. await service.remove_unavailable_chat(-100)
  51. assert caught.value.code == "chat_still_accessible"
  52. assert caught.value.status == 409
  53. assert await app_modules.wbb.db.managed_chats.find_one({"chat_id": -100})
  54. async def test_remove_unavailable_chat_reports_missing_record(app_modules):
  55. service = app_modules.load("wbb.services.chat_management")
  56. with pytest.raises(service.ChatManagementError) as caught:
  57. await service.remove_unavailable_chat(-100)
  58. assert caught.value.code == "chat_not_found"
  59. assert caught.value.status == 404
  60. async def test_profile_update_does_not_refresh_after_success(app_modules, monkeypatch):
  61. service = app_modules.load("wbb.services.chat_management")
  62. ensure_permission = AsyncMock()
  63. sync_managed_chat = AsyncMock(return_value=chat_overview())
  64. set_chat_title = AsyncMock()
  65. set_chat_description = AsyncMock()
  66. monkeypatch.setattr(service, "ensure_permission", ensure_permission)
  67. monkeypatch.setattr(service, "sync_managed_chat", sync_managed_chat)
  68. monkeypatch.setattr(service.app, "set_chat_title", set_chat_title, raising=False)
  69. monkeypatch.setattr(
  70. service.app, "set_chat_description", set_chat_description, raising=False
  71. )
  72. result = await service.update_chat_profile(
  73. -100, title="新标题", description="新描述"
  74. )
  75. ensure_permission.assert_awaited_once_with(
  76. -100, "can_change_info", actor_id=None
  77. )
  78. sync_managed_chat.assert_awaited_once_with(-100)
  79. set_chat_title.assert_awaited_once_with(-100, "新标题")
  80. set_chat_description.assert_awaited_once_with(-100, "新描述")
  81. assert result["title"] == "新标题"
  82. assert result["description"] == "新描述"
  83. async def test_profile_update_skips_values_already_applied(app_modules, monkeypatch):
  84. service = app_modules.load("wbb.services.chat_management")
  85. monkeypatch.setattr(service, "ensure_permission", AsyncMock())
  86. monkeypatch.setattr(
  87. service, "sync_managed_chat", AsyncMock(return_value=chat_overview())
  88. )
  89. set_chat_title = AsyncMock()
  90. set_chat_description = AsyncMock()
  91. monkeypatch.setattr(service.app, "set_chat_title", set_chat_title, raising=False)
  92. monkeypatch.setattr(
  93. service.app, "set_chat_description", set_chat_description, raising=False
  94. )
  95. result = await service.update_chat_profile(
  96. -100, title=" 旧标题 ", description="旧描述"
  97. )
  98. set_chat_title.assert_not_awaited()
  99. set_chat_description.assert_not_awaited()
  100. assert result["title"] == "旧标题"
  101. assert result["description"] == "旧描述"
  102. async def test_profile_update_accepts_chat_not_modified(app_modules, monkeypatch):
  103. service = app_modules.load("wbb.services.chat_management")
  104. monkeypatch.setattr(service, "ensure_permission", AsyncMock())
  105. monkeypatch.setattr(
  106. service, "sync_managed_chat", AsyncMock(return_value=chat_overview())
  107. )
  108. set_chat_title = AsyncMock(side_effect=ChatNotModified())
  109. monkeypatch.setattr(service.app, "set_chat_title", set_chat_title, raising=False)
  110. result = await service.update_chat_profile(-100, title="新标题")
  111. assert result["title"] == "新标题"
  112. async def test_profile_update_wraps_real_telegram_failure(app_modules, monkeypatch):
  113. service = app_modules.load("wbb.services.chat_management")
  114. monkeypatch.setattr(service, "ensure_permission", AsyncMock())
  115. monkeypatch.setattr(
  116. service, "sync_managed_chat", AsyncMock(return_value=chat_overview())
  117. )
  118. monkeypatch.setattr(
  119. service.app,
  120. "set_chat_title",
  121. AsyncMock(side_effect=RuntimeError("telegram unavailable")),
  122. raising=False,
  123. )
  124. with pytest.raises(service.ChatManagementError) as caught:
  125. await service.update_chat_profile(-100, title="新标题")
  126. assert caught.value.code == "chat_profile_update_failed"
  127. assert caught.value.status == 502
  128. assert str(caught.value) == "Telegram 未能更新群标题,请稍后重试。"
  129. async def test_automation_settings_store_configurable_risk_rules(
  130. app_modules, monkeypatch
  131. ):
  132. service = app_modules.load("wbb.services.chat_management")
  133. monkeypatch.setattr(service, "ensure_permission", AsyncMock())
  134. result = await service.apply_automation_settings(
  135. -100,
  136. {
  137. "risk_rules": [
  138. {
  139. "rule_id": "ads",
  140. "name": "广告内容",
  141. "enabled": True,
  142. "keywords": ["广告", "做爱"],
  143. "match_images": True,
  144. "match_links": True,
  145. "actions": ["delete", "kick"],
  146. "duration_seconds": 7200,
  147. },
  148. {
  149. "rule_id": "disabled-links",
  150. "name": "停用链接规则",
  151. "enabled": False,
  152. "keywords": ["未生效"],
  153. "match_links": True,
  154. "actions": ["delete"],
  155. "duration_seconds": 3600,
  156. },
  157. ],
  158. "identity_monitor": {"enabled": True, "notify_in_chat": True},
  159. },
  160. )
  161. assert result["risk_rules"][0] == {
  162. "rule_id": "ads",
  163. "name": "广告内容",
  164. "enabled": True,
  165. "keywords": ["做爱", "广告"],
  166. "match_images": True,
  167. "match_links": True,
  168. "actions": ["delete", "kick"],
  169. "duration_seconds": 7200,
  170. }
  171. assert len(result["risk_rules"]) == 2
  172. assert result["identity_monitor"] == {"enabled": True, "notify_in_chat": True}
  173. assert await service.get_blacklisted_words(-100) == ["做爱", "广告"]
  174. async def test_automation_settings_reject_conflicting_member_actions(
  175. app_modules, monkeypatch
  176. ):
  177. service = app_modules.load("wbb.services.chat_management")
  178. monkeypatch.setattr(service, "ensure_permission", AsyncMock())
  179. with pytest.raises(service.ChatManagementError) as caught:
  180. await service.apply_automation_settings(
  181. -100,
  182. {
  183. "risk_rules": [
  184. {
  185. "rule_id": "ads",
  186. "name": "广告内容",
  187. "enabled": True,
  188. "keywords": ["广告"],
  189. "actions": ["warn", "ban"],
  190. }
  191. ]
  192. },
  193. )
  194. assert caught.value.code == "invalid_risk_rules"
  195. async def test_automation_settings_wrap_legacy_policy_and_blacklist(app_modules):
  196. service = app_modules.load("wbb.services.chat_management")
  197. dbadmin = app_modules.load("wbb.utils.dbadmin")
  198. dbfunctions = app_modules.load("wbb.utils.dbfunctions")
  199. await dbadmin.update_managed_chat_settings(
  200. -100,
  201. {
  202. "risk_control": {
  203. "enabled": True,
  204. "keywords": ["广告"],
  205. "match_links": True,
  206. "actions": ["delete", "ban"],
  207. }
  208. },
  209. )
  210. await dbfunctions.save_blacklist_filter(-100, "推广")
  211. result = await service.get_automation_settings(-100)
  212. assert "risk_control" not in result
  213. assert result["risk_rules"] == [
  214. {
  215. "rule_id": "legacy-risk-control",
  216. "name": "原风控策略",
  217. "enabled": True,
  218. "keywords": ["广告", "推广"],
  219. "match_images": False,
  220. "match_links": True,
  221. "actions": ["delete", "ban"],
  222. "duration_seconds": 3600,
  223. }
  224. ]
  225. async def test_member_search_accepts_display_name_from_recent_activity(
  226. app_modules, monkeypatch
  227. ):
  228. service = app_modules.load("wbb.services.chat_management")
  229. dbadmin = app_modules.load("wbb.utils.dbadmin")
  230. await dbadmin.upsert_recent_chat_member(
  231. chat_id=-100,
  232. user_id=42,
  233. username=None,
  234. first_name="小明",
  235. last_name="同学",
  236. is_bot=False,
  237. )
  238. async def no_telegram_results():
  239. if False:
  240. yield None
  241. monkeypatch.setattr(
  242. service.app,
  243. "get_chat_members",
  244. lambda *_args, **_kwargs: no_telegram_results(),
  245. raising=False,
  246. )
  247. app_modules.app.members[(-100, 42)] = type(
  248. "Member",
  249. (),
  250. {
  251. "status": ChatMemberStatus.MEMBER,
  252. "privileges": None,
  253. "user": type(
  254. "User",
  255. (),
  256. {
  257. "id": 42,
  258. "username": None,
  259. "first_name": "小明",
  260. "last_name": "同学",
  261. "is_bot": False,
  262. "is_deleted": False,
  263. },
  264. )(),
  265. "custom_title": None,
  266. "until_date": None,
  267. },
  268. )()
  269. results = await service.search_chat_members(-100, "小明")
  270. assert [item["user"]["id"] for item in results] == ["42"]