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.
151 lines
4.8 KiB
Python
151 lines
4.8 KiB
Python
"""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]
|