Auto-Rename / tests /test_web_main.py
github-actions[bot]
Automated sync from GitHub
a00abaf
Raw History Blame Contribute Delete
8.52 kB
import asyncio
import dataclasses
import logging
import socket
from types import SimpleNamespace
import aiohttp
import pytest
from aiohttp.test_utils import TestClient, TestServer
from telethon import functions
from telethon.crypto import AuthKey
import bot.main as bot_main
from bot import __version__
from bot.config import load_config
from bot.db import Database
from bot.errorlog import ChannelLogHandler
from bot.web import make_app, start_web
from .tgfakes import FakeClient
def free_port() -> int:
with socket.socket() as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
# ---------------------------------------------------------------------------
# health server
# ---------------------------------------------------------------------------
async def test_health_endpoints_answer_get_and_head():
async with TestClient(TestServer(make_app())) as client:
r = await client.get("/")
assert r.status == 200 and "running" in await r.text()
assert (await client.head("/")).status == 200 # cron pingers often use HEAD
r = await client.get("/health")
body = await r.json()
assert r.status == 200 and body["status"] == "ok" and body["version"] == __version__
assert body["uptime_seconds"] >= 0
assert (await client.get("/nope")).status == 404
async def test_start_web_listens_on_the_given_port():
port = free_port()
runner = await start_web(port, host="127.0.0.1")
try:
async with aiohttp.ClientSession() as http:
async with http.get(f"http://127.0.0.1:{port}/health") as r:
assert r.status == 200
finally:
await runner.cleanup()
# ---------------------------------------------------------------------------
# start-up helpers
# ---------------------------------------------------------------------------
def test_clean_temp_dir_removes_only_job_leftovers(tmp_path):
root = tmp_path / "work"
(root / "job1").mkdir(parents=True)
(root / "job1" / "movie.mkv").write_bytes(b"1")
(root / "tracks5_6").mkdir()
(root / "tracks5_6" / "probe.mkv").write_bytes(b"2")
(root / "other").mkdir()
(root / "keep.txt").write_text("k")
bot_main.clean_temp_dir(str(root))
assert sorted(p.name for p in root.iterdir()) == ["keep.txt", "other"]
def test_clean_temp_dir_creates_a_missing_directory(tmp_path):
bot_main.clean_temp_dir(str(tmp_path / "a" / "b"))
assert (tmp_path / "a" / "b").is_dir()
def test_main_exits_with_status_2_when_configuration_is_missing(monkeypatch, capsys):
for key in ("API_ID", "API_HASH", "BOT_TOKEN", "OWNER_ID"):
monkeypatch.delenv(key, raising=False)
with pytest.raises(SystemExit) as exc:
bot_main.main()
assert exc.value.code == 2
assert "Missing required environment variables" in capsys.readouterr().err
class FakeTG:
created: list = []
def __init__(self, session, api_id, api_hash, **kw):
self.session, self.api_id, self.api_hash, self.kw = session, api_id, api_hash, kw
self.had_key = bool(session.auth_key)
self.started_with = None
FakeTG.created.append(self)
async def start(self, bot_token=None):
self.started_with = bot_token
if not self.session.auth_key: # a fresh login creates an auth key
self.session.set_dc(2, "149.154.167.51", 443)
self.session.auth_key = AuthKey(b"\x07" * 256)
return self
@pytest.fixture
async def login(tmp_path, monkeypatch):
FakeTG.created = []
monkeypatch.setattr("telethon.TelegramClient", FakeTG)
cfg = load_config({"API_ID": "11", "API_HASH": "hash", "BOT_TOKEN": "1234:abc", "OWNER_ID": "1"})
db = Database(f"sqlite+aiosqlite:///{tmp_path}/login.db")
await db.init()
yield SimpleNamespace(cfg=cfg, db=db)
await db.close()
async def test_first_login_saves_the_session_and_later_starts_reuse_it(login):
first = await bot_main.build_client(login.cfg, login.db)
assert not first.had_key and first.started_with == "1234:abc" and (first.api_id, first.api_hash) == (11, "hash")
assert first.kw["connection_retries"] == -1 and first.kw["auto_reconnect"] is True
assert await login.db.kv_get("session") and await login.db.kv_get("session_bot") == "1234"
second = await bot_main.build_client(login.cfg, login.db) # e.g. after the Space restarted
assert second.had_key and second.session.dc_id == 2
async def test_a_saved_session_of_another_bot_is_not_reused(login):
await bot_main.build_client(login.cfg, login.db)
other = dataclasses.replace(login.cfg, bot_token="9999:zzz")
client = await bot_main.build_client(other, login.db)
assert not client.had_key and await login.db.kv_get("session_bot") == "9999"
async def test_session_string_setting_wins_over_the_database(login):
saved = (await bot_main.build_client(login.cfg, login.db)).session.save()
await login.db.kv_delete("session")
await login.db.kv_delete("session_bot")
cfg = dataclasses.replace(login.cfg, session_string=saved)
assert (await bot_main.build_client(cfg, login.db)).had_key
# ---------------------------------------------------------------------------
# whole start-up sequence with a fake Telegram client
# ---------------------------------------------------------------------------
async def test_amain_wires_everything_and_exits_nonzero_if_telegram_disconnects(tmp_path, monkeypatch):
port = free_port()
env = {
"API_ID": "1", "API_HASH": "h", "BOT_TOKEN": "1:t", "OWNER_ID": "1000001", "LOG_CHANNEL": "-1001111111111",
"DATABASE_URL": f"sqlite+aiosqlite:///{tmp_path}/main.db", "TEMP_DIR": str(tmp_path / "work"),
"PORT": str(port), "FAST_TRANSFER": "0",
}
for key, value in env.items():
monkeypatch.setenv(key, value)
(tmp_path / "work" / "job9").mkdir(parents=True) # leftover of a "crashed" run
gate = asyncio.Event()
client = FakeClient()
client.disconnected = False
async def get_me():
return SimpleNamespace(username="rename_bot")
async def run_until_disconnected():
await gate.wait()
async def disconnect():
client.disconnected = True
client.get_me, client.run_until_disconnected, client.disconnect = get_me, run_until_disconnected, disconnect
async def fake_build_client(cfg, db):
return client
monkeypatch.setattr(bot_main, "build_client", fake_build_client)
task = asyncio.ensure_future(bot_main.amain())
try:
for _ in range(200): # wait for the "started" notice: everything is wired by then
if any("Bot started" in text for _chat, text, _kw in client.sent):
break
await asyncio.sleep(0.05)
else:
pytest.fail("the bot never finished starting")
async with aiohttp.ClientSession() as http:
async with http.get(f"http://127.0.0.1:{port}/health") as r:
assert r.status == 200
assert len(client.handlers) > 10
assert any(isinstance(r, functions.bots.SetBotCommandsRequest) for r in client.requests)
assert not (tmp_path / "work" / "job9").exists()
assert client.sent[0][0] == -1001111111111
started = client.sent[0][1] # the notice also says what the speed depends on (here: parallel transfer is off)
assert "Bot started" in started and "⚡ cryptg ✅" in started and "parallel off ⚠️" in started
assert any(isinstance(h, ChannelLogHandler) for h in logging.getLogger().handlers)
logging.getLogger("bot.something").error("an error somewhere in the bot") # errors go to the log channel as well
for _ in range(200):
if any("an error somewhere in the bot" in text for _chat, text, _kw in client.sent):
break
await asyncio.sleep(0.05)
else:
pytest.fail("the error never reached the log channel")
gate.set() # Telegram connection lost -> exit non-zero so the container restarts
assert await asyncio.wait_for(task, 10) == 1
finally:
gate.set()
if not task.done():
task.cancel()
assert client.disconnected
assert not any(isinstance(h, ChannelLogHandler) for h in logging.getLogger().handlers) # detached again on the way out
with pytest.raises(aiohttp.ClientError):
async with aiohttp.ClientSession() as http:
async with http.get(f"http://127.0.0.1:{port}/health"):
pass