feat: add canary, tcping, archive, resolve plugins
canary: generate realistic fake credentials (token/aws/basic) for planting as canary tripwires. Per-channel state persistence. tcping: TCP connect latency probe through SOCKS5 proxy with min/avg/max reporting. Proxy-compatible alternative to traceroute. archive: save URLs to Wayback Machine via Save Page Now API, routed through SOCKS5 proxy. resolve: bulk DNS resolution (up to 10 hosts) via TCP DNS through SOCKS5 proxy with concurrent asyncio.gather. 83 new tests (1010 total), docs updated.
This commit is contained in:
150
tests/test_archive.py
Normal file
150
tests/test_archive.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Tests for the Wayback Machine archive plugin."""
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import sys
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from derp.irc import Message
|
||||
|
||||
# plugins/ is not a Python package -- load the module from file path
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"plugins.archive", Path(__file__).resolve().parent.parent / "plugins" / "archive.py",
|
||||
)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
sys.modules[_spec.name] = _mod
|
||||
_spec.loader.exec_module(_mod)
|
||||
|
||||
from plugins.archive import _save_page, cmd_archive # noqa: E402
|
||||
|
||||
# -- Helpers -----------------------------------------------------------------
|
||||
|
||||
class _FakeBot:
|
||||
def __init__(self):
|
||||
self.replied: list[str] = []
|
||||
|
||||
async def reply(self, message, text: str) -> None:
|
||||
self.replied.append(text)
|
||||
|
||||
|
||||
def _msg(text: str) -> Message:
|
||||
return Message(
|
||||
raw="", prefix="alice!~alice@host", nick="alice",
|
||||
command="PRIVMSG", params=["#test", text], tags={},
|
||||
)
|
||||
|
||||
|
||||
# -- _save_page --------------------------------------------------------------
|
||||
|
||||
class TestSavePage:
|
||||
def test_content_location_header(self):
|
||||
resp = MagicMock()
|
||||
resp.headers = {"Content-Location": "/web/20260220/https://example.com"}
|
||||
resp.read.return_value = b""
|
||||
resp.geturl.return_value = "https://web.archive.org/save/https://example.com"
|
||||
|
||||
with patch.object(_mod, "_urlopen", return_value=resp):
|
||||
result = _save_page("https://example.com")
|
||||
|
||||
assert "url" in result
|
||||
assert "/web/20260220" in result["url"]
|
||||
|
||||
def test_final_url_redirect(self):
|
||||
resp = MagicMock()
|
||||
resp.headers = {}
|
||||
resp.read.return_value = b""
|
||||
resp.geturl.return_value = "https://web.archive.org/web/20260220/https://example.com"
|
||||
|
||||
with patch.object(_mod, "_urlopen", return_value=resp):
|
||||
result = _save_page("https://example.com")
|
||||
|
||||
assert "url" in result
|
||||
assert "/web/20260220" in result["url"]
|
||||
|
||||
def test_fallback_url(self):
|
||||
resp = MagicMock()
|
||||
resp.headers = {}
|
||||
resp.read.return_value = b""
|
||||
resp.geturl.return_value = "https://web.archive.org/save/ok"
|
||||
|
||||
with patch.object(_mod, "_urlopen", return_value=resp):
|
||||
result = _save_page("https://example.com")
|
||||
|
||||
assert "url" in result
|
||||
assert "/web/*/" in result["url"]
|
||||
|
||||
def test_rate_limit(self):
|
||||
exc = urllib.error.HTTPError(
|
||||
"url", 429, "Too Many Requests", {}, None,
|
||||
)
|
||||
with patch.object(_mod, "_urlopen", side_effect=exc):
|
||||
result = _save_page("https://example.com")
|
||||
|
||||
assert "error" in result
|
||||
assert "rate limit" in result["error"]
|
||||
|
||||
def test_origin_unreachable(self):
|
||||
exc = urllib.error.HTTPError(
|
||||
"url", 523, "Origin Unreachable", {}, None,
|
||||
)
|
||||
with patch.object(_mod, "_urlopen", side_effect=exc):
|
||||
result = _save_page("https://example.com")
|
||||
|
||||
assert "error" in result
|
||||
assert "unreachable" in result["error"]
|
||||
|
||||
def test_generic_http_error(self):
|
||||
exc = urllib.error.HTTPError(
|
||||
"url", 500, "Server Error", {}, None,
|
||||
)
|
||||
with patch.object(_mod, "_urlopen", side_effect=exc):
|
||||
result = _save_page("https://example.com")
|
||||
|
||||
assert "error" in result
|
||||
assert "500" in result["error"]
|
||||
|
||||
def test_timeout(self):
|
||||
with patch.object(_mod, "_urlopen", side_effect=TimeoutError("timed out")):
|
||||
result = _save_page("https://example.com")
|
||||
|
||||
assert "error" in result
|
||||
assert "timeout" in result["error"]
|
||||
|
||||
|
||||
# -- Command handler ---------------------------------------------------------
|
||||
|
||||
class TestCmdArchive:
|
||||
def test_no_args(self):
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_archive(bot, _msg("!archive")))
|
||||
assert "Usage" in bot.replied[0]
|
||||
|
||||
def test_no_scheme(self):
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_archive(bot, _msg("!archive example.com")))
|
||||
assert "http://" in bot.replied[0]
|
||||
|
||||
def test_success(self):
|
||||
bot = _FakeBot()
|
||||
result = {"url": "https://web.archive.org/web/20260220/https://example.com"}
|
||||
|
||||
with patch.object(_mod, "_save_page", return_value=result):
|
||||
asyncio.run(cmd_archive(bot, _msg("!archive https://example.com")))
|
||||
|
||||
assert len(bot.replied) == 2
|
||||
assert "Archiving" in bot.replied[0]
|
||||
assert "Archived:" in bot.replied[1]
|
||||
assert "/web/20260220" in bot.replied[1]
|
||||
|
||||
def test_error(self):
|
||||
bot = _FakeBot()
|
||||
result = {"error": "rate limited -- try again later"}
|
||||
|
||||
with patch.object(_mod, "_save_page", return_value=result):
|
||||
asyncio.run(cmd_archive(bot, _msg("!archive https://example.com")))
|
||||
|
||||
assert len(bot.replied) == 2
|
||||
assert "failed" in bot.replied[1].lower()
|
||||
assert "rate limit" in bot.replied[1]
|
||||
Reference in New Issue
Block a user