Канонизация. Один ролик теперь даёт одну запись: youtu.be/X, youtube.com/watch?v=X и та же ссылка с тайм-кодом или списком воспроизведения раньше считались тремя разными роликами, и счётчик популярности размазывался. Заодно закрыта приватная проблема: из адреса вырезаются метки перехода и всё похожее на секреты (token, api_key, signature и прочее) — лента публичная, и публиковать чужой токен нельзя. Уже накопленные записи приводятся к новому виду при старте, счётчики схлопнувшихся строк складываются. /readyz. Прежний /healthz отвечал «жив» даже при полном диске, мёртвом уборщике, неработающей БД и отсутствующем ffmpeg. Новый эндпоинт проверяет всё это плюс глубину очереди и возраст последнего прохода уборщика, и при деградации отдаёт 503 — чтобы поломка была видна мониторингу, а не только пользователям. Тесты. 187 проверок, сеть не используется. Покрыты: селекторы форматов (дословные эталоны плюс компиляция всех сочетаний самим yt-dlp), валидация ссылок и SSRF, path traversal и символические ссылки, имена файлов с кириллицей, отсев форматов с неизвестными кодеками, счётчики и лента, HTTP-слой. В conftest пути принудительно уводятся во временный каталог: импорт app создаёт менеджер, который подметает каталог загрузок, и без этой предосторожности прогон тестов удалял файлы работающего сервиса — так уже случалось. Тесты сразу нашли две несогласованности, обе исправлены: * отмена несуществующей задачи отвечала 409, хотя соседние ручки на ту же задачу отвечают 404; * метки времени в ленте писались с точностью до секунды, поэтому порядок «недавних» среди записей одной секунды был произвольным. Также объединены карты сообщений об ошибках: они дублировались в двух ручках и успели разойтись — на одну и ту же ссылку /api/info отвечал «Этот адрес недоступен», а /api/downloads глотал код и говорил «Плохая ссылка». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwY2Cg54RK7WZdvMaLV5Di
72 lines
3 KiB
Python
72 lines
3 KiB
Python
"""Общая подготовка тестов.
|
||
|
||
ВАЖНО: переменные окружения выставляются ДО импорта config и app.
|
||
`import app` на уровне модуля создаёт менеджер загрузок, который при старте
|
||
подметает каталог загрузок. Если не перенаправить пути заранее, прогон
|
||
тестов удалит файлы работающего сервиса — так уже случалось.
|
||
"""
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
_TMP = Path(tempfile.mkdtemp(prefix="ytg-tests-"))
|
||
os.environ["YTG_DOWNLOAD_DIR"] = str(_TMP / "downloads")
|
||
os.environ["YTG_DATA_DIR"] = str(_TMP / "data")
|
||
os.environ["YTG_TRUST_PROXY"] = "0"
|
||
os.environ["YTG_JANITOR_INTERVAL_SEC"] = "3600" # уборщик не мешает тестам
|
||
os.environ["YTG_MAX_CONCURRENT"] = "2"
|
||
os.environ["YTG_QUEUE_MAX"] = "4"
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||
|
||
import pytest # noqa: E402
|
||
|
||
import config # noqa: E402
|
||
|
||
|
||
@pytest.fixture(scope="session", autouse=True)
|
||
def _guard_paths():
|
||
"""Страховка: тесты не должны работать с боевыми каталогами."""
|
||
assert str(config.DOWNLOAD_DIR).startswith(str(_TMP)), config.DOWNLOAD_DIR
|
||
assert str(config.DATA_DIR).startswith(str(_TMP)), config.DATA_DIR
|
||
yield
|
||
|
||
|
||
@pytest.fixture
|
||
def client():
|
||
import app as app_module
|
||
app_module.app.config["TESTING"] = True
|
||
app_module._hits.clear() # чистый rate limit на каждый тест
|
||
with app_module.app.test_client() as c:
|
||
yield c
|
||
|
||
|
||
@pytest.fixture
|
||
def fake_info():
|
||
"""Метаданные в том виде, в каком их отдают экстракторы.
|
||
|
||
Специально смешаны три случая: кодеки известны, кодеков нет совсем
|
||
("none") и кодеки неизвестны (None) — последний ломался.
|
||
"""
|
||
return {
|
||
"id": "abc123",
|
||
"title": "Тестовый ролик",
|
||
"uploader": "Автор",
|
||
"duration": 120,
|
||
"thumbnail": "https://example.com/t.jpg",
|
||
"webpage_url": "https://example.com/watch?v=abc123",
|
||
"extractor_key": "Test",
|
||
"formats": [
|
||
{"format_id": "137", "ext": "mp4", "vcodec": "avc1.640028",
|
||
"acodec": "none", "height": 1080, "tbr": 4000},
|
||
{"format_id": "140", "ext": "m4a", "vcodec": "none",
|
||
"acodec": "mp4a.40.2", "height": None, "abr": 128},
|
||
{"format_id": "hls-480", "ext": "mp4", "vcodec": None,
|
||
"acodec": None, "height": 480, "tbr": 1500},
|
||
{"format_id": "mp4-low", "ext": "mp4", "vcodec": None,
|
||
"acodec": None, "height": None, "tbr": 500},
|
||
{"format_id": "sb0", "ext": "mhtml", "vcodec": "none",
|
||
"acodec": "none", "format_note": "storyboard"},
|
||
],
|
||
}
|