fix: repair broken tests across alert, chanmgmt, and integration
- test_alert: remove stale _MAX_ANNOUNCE import/test, update _errors assertions for per-backend dict, fix announcement checks (action vs send), mock _fetch_og_batch in seeding tests, fix YouTube/SearX mock targets (urllib.request.urlopen), include keyword in fake data titles - test_chanmgmt: add _FakeState to _FakeBot (on_invite now persists) - test_integration: update help assertion for new output format 696 tests pass, 0 failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,6 @@ sys.modules[_spec.name] = _mod
|
|||||||
_spec.loader.exec_module(_mod)
|
_spec.loader.exec_module(_mod)
|
||||||
|
|
||||||
from plugins.alert import ( # noqa: E402
|
from plugins.alert import ( # noqa: E402
|
||||||
_MAX_ANNOUNCE,
|
|
||||||
_MAX_SEEN,
|
_MAX_SEEN,
|
||||||
_delete,
|
_delete,
|
||||||
_errors,
|
_errors,
|
||||||
@@ -153,18 +152,30 @@ class _FakeState:
|
|||||||
return sorted(self._store.get(plugin, {}).keys())
|
return sorted(self._store.get(plugin, {}).keys())
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeRegistry:
|
||||||
|
"""Minimal registry stand-in."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._modules: dict = {}
|
||||||
|
|
||||||
|
|
||||||
class _FakeBot:
|
class _FakeBot:
|
||||||
"""Minimal bot stand-in that captures sent/replied messages."""
|
"""Minimal bot stand-in that captures sent/replied messages."""
|
||||||
|
|
||||||
def __init__(self, *, admin: bool = False):
|
def __init__(self, *, admin: bool = False):
|
||||||
self.sent: list[tuple[str, str]] = []
|
self.sent: list[tuple[str, str]] = []
|
||||||
|
self.actions: list[tuple[str, str]] = []
|
||||||
self.replied: list[str] = []
|
self.replied: list[str] = []
|
||||||
self.state = _FakeState()
|
self.state = _FakeState()
|
||||||
|
self.registry = _FakeRegistry()
|
||||||
self._admin = admin
|
self._admin = admin
|
||||||
|
|
||||||
async def send(self, target: str, text: str) -> None:
|
async def send(self, target: str, text: str) -> None:
|
||||||
self.sent.append((target, text))
|
self.sent.append((target, text))
|
||||||
|
|
||||||
|
async def action(self, target: str, text: str) -> None:
|
||||||
|
self.actions.append((target, text))
|
||||||
|
|
||||||
async def reply(self, message, text: str) -> None:
|
async def reply(self, message, text: str) -> None:
|
||||||
self.replied.append(text)
|
self.replied.append(text)
|
||||||
|
|
||||||
@@ -199,21 +210,21 @@ def _clear() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _fake_yt(keyword):
|
def _fake_yt(keyword):
|
||||||
"""Fake YouTube backend returning two results."""
|
"""Fake YouTube backend returning two results (keyword in title)."""
|
||||||
return [
|
return [
|
||||||
{"id": "yt1", "title": "YT Result 1",
|
{"id": "yt1", "title": "YT test Result 1",
|
||||||
"url": "https://www.youtube.com/watch?v=yt1", "extra": ""},
|
"url": "https://www.youtube.com/watch?v=yt1", "extra": ""},
|
||||||
{"id": "yt2", "title": "YT Result 2",
|
{"id": "yt2", "title": "YT test Result 2",
|
||||||
"url": "https://www.youtube.com/watch?v=yt2", "extra": ""},
|
"url": "https://www.youtube.com/watch?v=yt2", "extra": ""},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _fake_tw(keyword):
|
def _fake_tw(keyword):
|
||||||
"""Fake Twitch backend returning two results."""
|
"""Fake Twitch backend returning two results (keyword in title)."""
|
||||||
return [
|
return [
|
||||||
{"id": "stream:tw1", "title": "TW Stream 1",
|
{"id": "stream:tw1", "title": "TW test Stream 1",
|
||||||
"url": "https://twitch.tv/user1", "extra": ""},
|
"url": "https://twitch.tv/user1", "extra": ""},
|
||||||
{"id": "vod:tw2", "title": "TW VOD 1",
|
{"id": "vod:tw2", "title": "TW test VOD 1",
|
||||||
"url": "https://twitch.tv/videos/tw2", "extra": ""},
|
"url": "https://twitch.tv/videos/tw2", "extra": ""},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -229,11 +240,11 @@ def _fake_tw_error(keyword):
|
|||||||
|
|
||||||
|
|
||||||
def _fake_sx(keyword):
|
def _fake_sx(keyword):
|
||||||
"""Fake SearX backend returning two results."""
|
"""Fake SearX backend returning two results (keyword in title)."""
|
||||||
return [
|
return [
|
||||||
{"id": "https://example.com/sx1", "title": "SX Result 1",
|
{"id": "https://example.com/sx1", "title": "SX test Result 1",
|
||||||
"url": "https://example.com/sx1", "extra": ""},
|
"url": "https://example.com/sx1", "extra": ""},
|
||||||
{"id": "https://example.com/sx2", "title": "SX Result 2",
|
{"id": "https://example.com/sx2", "title": "SX test Result 2",
|
||||||
"url": "https://example.com/sx2", "extra": ""},
|
"url": "https://example.com/sx2", "extra": ""},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -370,7 +381,7 @@ class TestExtractVideos:
|
|||||||
def close(self):
|
def close(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with patch.object(_mod, "_urlopen", return_value=FakeResp()):
|
with patch("urllib.request.urlopen", return_value=FakeResp()):
|
||||||
results = _search_youtube("test")
|
results = _search_youtube("test")
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert results[0]["id"] == "dup1"
|
assert results[0]["id"] == "dup1"
|
||||||
@@ -388,7 +399,7 @@ class TestSearchYoutube:
|
|||||||
def close(self):
|
def close(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with patch.object(_mod, "_urlopen", return_value=FakeResp()):
|
with patch("urllib.request.urlopen", return_value=FakeResp()):
|
||||||
results = _search_youtube("test query")
|
results = _search_youtube("test query")
|
||||||
assert len(results) == 2
|
assert len(results) == 2
|
||||||
assert results[0]["id"] == "abc123"
|
assert results[0]["id"] == "abc123"
|
||||||
@@ -396,7 +407,7 @@ class TestSearchYoutube:
|
|||||||
|
|
||||||
def test_http_error_propagates(self):
|
def test_http_error_propagates(self):
|
||||||
import pytest
|
import pytest
|
||||||
with patch.object(_mod, "_urlopen", side_effect=ConnectionError("fail")):
|
with patch("urllib.request.urlopen", side_effect=ConnectionError("fail")):
|
||||||
with pytest.raises(ConnectionError):
|
with pytest.raises(ConnectionError):
|
||||||
_search_youtube("test")
|
_search_youtube("test")
|
||||||
|
|
||||||
@@ -529,26 +540,28 @@ class TestCmdAlertAdd:
|
|||||||
bot = _FakeBot(admin=True)
|
bot = _FakeBot(admin=True)
|
||||||
|
|
||||||
async def inner():
|
async def inner():
|
||||||
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
with (
|
||||||
|
patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS),
|
||||||
|
patch.object(_mod, "_fetch_og_batch", return_value={}),
|
||||||
|
):
|
||||||
await cmd_alert(bot, _msg("!alert add mc-speed minecraft speedrun"))
|
await cmd_alert(bot, _msg("!alert add mc-speed minecraft speedrun"))
|
||||||
await asyncio.sleep(0)
|
# Allow background seeding task to complete (patches must stay active)
|
||||||
assert len(bot.replied) == 1
|
await asyncio.sleep(0.2)
|
||||||
assert "Alert 'mc-speed' added" in bot.replied[0]
|
assert len(bot.replied) == 1
|
||||||
assert "minecraft speedrun" in bot.replied[0]
|
assert "Alert 'mc-speed' added" in bot.replied[0]
|
||||||
assert "2 yt" in bot.replied[0]
|
assert "minecraft speedrun" in bot.replied[0]
|
||||||
assert "2 tw" in bot.replied[0]
|
data = _load(bot, "#test:mc-speed")
|
||||||
assert "2 sx" in bot.replied[0]
|
assert data is not None
|
||||||
data = _load(bot, "#test:mc-speed")
|
assert data["name"] == "mc-speed"
|
||||||
assert data is not None
|
assert data["keyword"] == "minecraft speedrun"
|
||||||
assert data["name"] == "mc-speed"
|
assert data["channel"] == "#test"
|
||||||
assert data["keyword"] == "minecraft speedrun"
|
# Seeding happens in background; verify seen lists populated
|
||||||
assert data["channel"] == "#test"
|
assert len(data["seen"]["yt"]) == 2
|
||||||
assert len(data["seen"]["yt"]) == 2
|
assert len(data["seen"]["tw"]) == 2
|
||||||
assert len(data["seen"]["tw"]) == 2
|
assert len(data["seen"]["sx"]) == 2
|
||||||
assert len(data["seen"]["sx"]) == 2
|
assert "#test:mc-speed" in _pollers
|
||||||
assert "#test:mc-speed" in _pollers
|
_stop_poller("#test:mc-speed")
|
||||||
_stop_poller("#test:mc-speed")
|
await asyncio.sleep(0)
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
asyncio.run(inner())
|
asyncio.run(inner())
|
||||||
|
|
||||||
@@ -590,7 +603,7 @@ class TestCmdAlertAdd:
|
|||||||
async def inner():
|
async def inner():
|
||||||
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
||||||
await cmd_alert(bot, _msg("!alert add dupe some keyword"))
|
await cmd_alert(bot, _msg("!alert add dupe some keyword"))
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0.1)
|
||||||
bot.replied.clear()
|
bot.replied.clear()
|
||||||
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
||||||
await cmd_alert(bot, _msg("!alert add dupe other keyword"))
|
await cmd_alert(bot, _msg("!alert add dupe other keyword"))
|
||||||
@@ -620,16 +633,20 @@ class TestCmdAlertAdd:
|
|||||||
backends = {"yt": _fake_yt, "tw": _fake_tw_error, "sx": _fake_sx}
|
backends = {"yt": _fake_yt, "tw": _fake_tw_error, "sx": _fake_sx}
|
||||||
|
|
||||||
async def inner():
|
async def inner():
|
||||||
with patch.object(_mod, "_BACKENDS", backends):
|
with (
|
||||||
|
patch.object(_mod, "_BACKENDS", backends),
|
||||||
|
patch.object(_mod, "_fetch_og_batch", return_value={}),
|
||||||
|
):
|
||||||
await cmd_alert(bot, _msg("!alert add partial test keyword"))
|
await cmd_alert(bot, _msg("!alert add partial test keyword"))
|
||||||
await asyncio.sleep(0)
|
# Allow background seeding task to complete (patches must stay active)
|
||||||
data = _load(bot, "#test:partial")
|
await asyncio.sleep(0.2)
|
||||||
assert data is not None
|
data = _load(bot, "#test:partial")
|
||||||
assert len(data["seen"]["yt"]) == 2
|
assert data is not None
|
||||||
assert len(data["seen"]["tw"]) == 0
|
assert len(data["seen"]["yt"]) == 2
|
||||||
assert len(data["seen"]["sx"]) == 2
|
assert len(data["seen"].get("tw", [])) == 0
|
||||||
_stop_poller("#test:partial")
|
assert len(data["seen"]["sx"]) == 2
|
||||||
await asyncio.sleep(0)
|
_stop_poller("#test:partial")
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
asyncio.run(inner())
|
asyncio.run(inner())
|
||||||
|
|
||||||
@@ -646,7 +663,7 @@ class TestCmdAlertDel:
|
|||||||
async def inner():
|
async def inner():
|
||||||
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
||||||
await cmd_alert(bot, _msg("!alert add todel some keyword"))
|
await cmd_alert(bot, _msg("!alert add todel some keyword"))
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0.1)
|
||||||
bot.replied.clear()
|
bot.replied.clear()
|
||||||
await cmd_alert(bot, _msg("!alert del todel"))
|
await cmd_alert(bot, _msg("!alert del todel"))
|
||||||
assert "Removed 'todel'" in bot.replied[0]
|
assert "Removed 'todel'" in bot.replied[0]
|
||||||
@@ -713,10 +730,11 @@ class TestCmdAlertList:
|
|||||||
bot = _FakeBot()
|
bot = _FakeBot()
|
||||||
_save(bot, "#test:broken", {
|
_save(bot, "#test:broken", {
|
||||||
"name": "broken", "channel": "#test", "keyword": "test",
|
"name": "broken", "channel": "#test", "keyword": "test",
|
||||||
"last_error": "Connection refused",
|
"last_errors": {"yt": "Connection refused"},
|
||||||
})
|
})
|
||||||
asyncio.run(cmd_alert(bot, _msg("!alert list")))
|
asyncio.run(cmd_alert(bot, _msg("!alert list")))
|
||||||
assert "broken (error)" in bot.replied[0]
|
assert "broken" in bot.replied[0]
|
||||||
|
assert "backend error" in bot.replied[0]
|
||||||
|
|
||||||
def test_list_requires_channel(self):
|
def test_list_requires_channel(self):
|
||||||
_clear()
|
_clear()
|
||||||
@@ -809,10 +827,11 @@ class TestCmdAlertCheck:
|
|||||||
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
||||||
await cmd_alert(bot, _msg("!alert check news"))
|
await cmd_alert(bot, _msg("!alert check news"))
|
||||||
# yt2 is new for yt, both tw and sx results are new
|
# yt2 is new for yt, both tw and sx results are new
|
||||||
announcements = [s for t, s in bot.sent if t == "#test"]
|
# Metadata (with backend tags) goes to action(), titles to send()
|
||||||
yt_msgs = [m for m in announcements if "/yt]" in m]
|
actions = [s for t, s in bot.actions if t == "#test"]
|
||||||
tw_msgs = [m for m in announcements if "/tw]" in m]
|
yt_msgs = [m for m in actions if "/yt/" in m]
|
||||||
sx_msgs = [m for m in announcements if "/sx]" in m]
|
tw_msgs = [m for m in actions if "/tw/" in m]
|
||||||
|
sx_msgs = [m for m in actions if "/sx/" in m]
|
||||||
assert len(yt_msgs) == 1 # yt2 only
|
assert len(yt_msgs) == 1 # yt2 only
|
||||||
assert len(tw_msgs) == 2 # both tw results
|
assert len(tw_msgs) == 2 # both tw results
|
||||||
assert len(sx_msgs) == 2 # both sx results
|
assert len(sx_msgs) == 2 # both sx results
|
||||||
@@ -846,11 +865,14 @@ class TestPollOnce:
|
|||||||
async def inner():
|
async def inner():
|
||||||
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
||||||
await _poll_once(bot, key, announce=True)
|
await _poll_once(bot, key, announce=True)
|
||||||
messages = [s for t, s in bot.sent if t == "#test"]
|
# Titles go to send(), metadata goes to action()
|
||||||
assert len(messages) == 6 # 2 yt + 2 tw + 2 sx
|
titles = [s for t, s in bot.sent if t == "#test"]
|
||||||
assert "[poll/yt]" in messages[0]
|
actions = [s for t, s in bot.actions if t == "#test"]
|
||||||
assert "[poll/tw]" in messages[2]
|
assert len(titles) == 6 # 2 yt + 2 tw + 2 sx
|
||||||
assert "[poll/sx]" in messages[4]
|
assert len(actions) == 6
|
||||||
|
assert "[poll/yt/" in actions[0]
|
||||||
|
assert "[poll/tw/" in actions[2]
|
||||||
|
assert "[poll/sx/" in actions[4]
|
||||||
|
|
||||||
asyncio.run(inner())
|
asyncio.run(inner())
|
||||||
|
|
||||||
@@ -877,36 +899,6 @@ class TestPollOnce:
|
|||||||
|
|
||||||
asyncio.run(inner())
|
asyncio.run(inner())
|
||||||
|
|
||||||
def test_max_announce_per_platform(self):
|
|
||||||
"""Only MAX_ANNOUNCE items per platform, then '... and N more'."""
|
|
||||||
_clear()
|
|
||||||
bot = _FakeBot()
|
|
||||||
|
|
||||||
def fake_many(keyword):
|
|
||||||
return [
|
|
||||||
{"id": f"v{i}", "title": f"Video {i}",
|
|
||||||
"url": f"https://example.com/{i}", "extra": ""}
|
|
||||||
for i in range(8)
|
|
||||||
]
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"keyword": "test", "name": "many", "channel": "#test",
|
|
||||||
"interval": 300, "seen": {"yt": [], "tw": [], "sx": []},
|
|
||||||
"last_poll": "", "last_error": "",
|
|
||||||
}
|
|
||||||
key = "#test:many"
|
|
||||||
_save(bot, key, data)
|
|
||||||
_subscriptions[key] = data
|
|
||||||
|
|
||||||
async def inner():
|
|
||||||
with patch.object(_mod, "_BACKENDS", {"yt": fake_many, "tw": _fake_tw}):
|
|
||||||
await _poll_once(bot, key, announce=True)
|
|
||||||
yt_msgs = [s for t, s in bot.sent if t == "#test" and "/yt]" in s]
|
|
||||||
assert len(yt_msgs) == _MAX_ANNOUNCE + 1 # 5 items + "... and 3 more"
|
|
||||||
assert "... and 3 more" in yt_msgs[-1]
|
|
||||||
|
|
||||||
asyncio.run(inner())
|
|
||||||
|
|
||||||
def test_partial_backend_failure(self):
|
def test_partial_backend_failure(self):
|
||||||
"""One backend fails, other still works. Error counter increments."""
|
"""One backend fails, other still works. Error counter increments."""
|
||||||
_clear()
|
_clear()
|
||||||
@@ -925,14 +917,14 @@ class TestPollOnce:
|
|||||||
with patch.object(_mod, "_BACKENDS", backends):
|
with patch.object(_mod, "_BACKENDS", backends):
|
||||||
await _poll_once(bot, key, announce=True)
|
await _poll_once(bot, key, announce=True)
|
||||||
# Twitch and SearX results should still be announced
|
# Twitch and SearX results should still be announced
|
||||||
tw_msgs = [s for t, s in bot.sent if t == "#test" and "/tw]" in s]
|
tw_msgs = [s for t, s in bot.actions if t == "#test" and "/tw/" in s]
|
||||||
sx_msgs = [s for t, s in bot.sent if t == "#test" and "/sx]" in s]
|
sx_msgs = [s for t, s in bot.actions if t == "#test" and "/sx/" in s]
|
||||||
assert len(tw_msgs) == 2
|
assert len(tw_msgs) == 2
|
||||||
assert len(sx_msgs) == 2
|
assert len(sx_msgs) == 2
|
||||||
# Error counter should be incremented
|
# Error counter should be incremented for yt backend
|
||||||
assert _errors[key] == 1
|
assert _errors[key]["yt"] == 1
|
||||||
updated = _load(bot, key)
|
updated = _load(bot, key)
|
||||||
assert "yt:" in updated["last_error"]
|
assert "yt" in updated.get("last_errors", {})
|
||||||
|
|
||||||
asyncio.run(inner())
|
asyncio.run(inner())
|
||||||
|
|
||||||
@@ -1005,7 +997,7 @@ class TestPollOnce:
|
|||||||
async def inner():
|
async def inner():
|
||||||
with patch.object(_mod, "_BACKENDS", backends):
|
with patch.object(_mod, "_BACKENDS", backends):
|
||||||
await _poll_once(bot, key, announce=True)
|
await _poll_once(bot, key, announce=True)
|
||||||
assert _errors[key] == 1
|
assert all(v == 1 for v in _errors[key].values())
|
||||||
assert len(bot.sent) == 0
|
assert len(bot.sent) == 0
|
||||||
|
|
||||||
asyncio.run(inner())
|
asyncio.run(inner())
|
||||||
@@ -1019,19 +1011,19 @@ class TestPollOnce:
|
|||||||
"yt": ["yt1", "yt2"], "tw": ["stream:tw1", "vod:tw2"],
|
"yt": ["yt1", "yt2"], "tw": ["stream:tw1", "vod:tw2"],
|
||||||
"sx": ["https://example.com/sx1", "https://example.com/sx2"],
|
"sx": ["https://example.com/sx1", "https://example.com/sx2"],
|
||||||
},
|
},
|
||||||
"last_poll": "", "last_error": "old error",
|
"last_poll": "", "last_errors": {"yt": "old error"},
|
||||||
}
|
}
|
||||||
key = "#test:clrerr"
|
key = "#test:clrerr"
|
||||||
_save(bot, key, data)
|
_save(bot, key, data)
|
||||||
_subscriptions[key] = data
|
_subscriptions[key] = data
|
||||||
_errors[key] = 3
|
_errors[key] = {"yt": 3, "tw": 3, "sx": 3}
|
||||||
|
|
||||||
async def inner():
|
async def inner():
|
||||||
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
with patch.object(_mod, "_BACKENDS", _FAKE_BACKENDS):
|
||||||
await _poll_once(bot, key, announce=True)
|
await _poll_once(bot, key, announce=True)
|
||||||
assert _errors[key] == 0
|
assert all(v == 0 for v in _errors[key].values())
|
||||||
updated = _load(bot, key)
|
updated = _load(bot, key)
|
||||||
assert updated["last_error"] == ""
|
assert updated.get("last_errors", {}) == {}
|
||||||
|
|
||||||
asyncio.run(inner())
|
asyncio.run(inner())
|
||||||
|
|
||||||
@@ -1222,6 +1214,7 @@ class TestSearchSearx:
|
|||||||
|
|
||||||
with patch("urllib.request.urlopen", return_value=FakeResp()):
|
with patch("urllib.request.urlopen", return_value=FakeResp()):
|
||||||
results = _search_searx("test query")
|
results = _search_searx("test query")
|
||||||
|
# Same response served for all categories; deduped by URL
|
||||||
assert len(results) == 3
|
assert len(results) == 3
|
||||||
assert results[0]["id"] == "https://example.com/sx1"
|
assert results[0]["id"] == "https://example.com/sx1"
|
||||||
assert results[0]["title"] == "SearX Result 1"
|
assert results[0]["title"] == "SearX Result 1"
|
||||||
@@ -1241,9 +1234,8 @@ class TestSearchSearx:
|
|||||||
results = _search_searx("nothing")
|
results = _search_searx("nothing")
|
||||||
assert results == []
|
assert results == []
|
||||||
|
|
||||||
def test_http_error_propagates(self):
|
def test_http_error_returns_empty(self):
|
||||||
import pytest
|
"""SearXNG catches per-category errors; all failing returns empty."""
|
||||||
|
|
||||||
with patch("urllib.request.urlopen", side_effect=ConnectionError("fail")):
|
with patch("urllib.request.urlopen", side_effect=ConnectionError("fail")):
|
||||||
with pytest.raises(ConnectionError):
|
results = _search_searx("test")
|
||||||
_search_searx("test")
|
assert results == []
|
||||||
|
|||||||
@@ -31,6 +31,26 @@ class _FakeConn:
|
|||||||
self.sent.append(raw)
|
self.sent.append(raw)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeState:
|
||||||
|
"""In-memory stand-in for bot.state."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._store: dict[str, dict[str, str]] = {}
|
||||||
|
|
||||||
|
def get(self, plugin: str, key: str, default: str | None = None) -> str | None:
|
||||||
|
return self._store.get(plugin, {}).get(key, default)
|
||||||
|
|
||||||
|
def set(self, plugin: str, key: str, value: str) -> None:
|
||||||
|
self._store.setdefault(plugin, {})[key] = value
|
||||||
|
|
||||||
|
def delete(self, plugin: str, key: str) -> bool:
|
||||||
|
try:
|
||||||
|
del self._store[plugin][key]
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class _FakeBot:
|
class _FakeBot:
|
||||||
"""Minimal bot stand-in."""
|
"""Minimal bot stand-in."""
|
||||||
|
|
||||||
@@ -38,6 +58,7 @@ class _FakeBot:
|
|||||||
self.joined: list[str] = []
|
self.joined: list[str] = []
|
||||||
self._admin = admin
|
self._admin = admin
|
||||||
self.conn = _FakeConn()
|
self.conn = _FakeConn()
|
||||||
|
self.state = _FakeState()
|
||||||
|
|
||||||
def _is_admin(self, message) -> bool:
|
def _is_admin(self, message) -> bool:
|
||||||
return self._admin
|
return self._admin
|
||||||
|
|||||||
@@ -227,8 +227,8 @@ class TestCommandDispatch:
|
|||||||
|
|
||||||
replies = h.sent_privmsgs("#test")
|
replies = h.sent_privmsgs("#test")
|
||||||
assert len(replies) == 1
|
assert len(replies) == 1
|
||||||
assert "Commands:" in replies[0]
|
assert "help" in replies[0]
|
||||||
assert "!ping" in replies[0]
|
assert "ping" in replies[0]
|
||||||
|
|
||||||
def test_unknown_command_ignored(self):
|
def test_unknown_command_ignored(self):
|
||||||
"""Unknown commands produce no reply."""
|
"""Unknown commands produce no reply."""
|
||||||
|
|||||||
Reference in New Issue
Block a user