yt-dlp-gui/tests/test_probe.py
codex-pve 2d2b138bbd Канонизация ссылок в ленте, эндпоинт /readyz и набор тестов
Канонизация. Один ролик теперь даёт одну запись: 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
2026-09-21 18:03:51 +03:00

102 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Разбор метаданных: какие форматы попадают в выдачу, а какие отсеиваются.
Здесь закреплена дорогая ошибка: у yt-dlp строка "none" означает, что
дорожки точно нет, а None — что кодек просто неизвестен. Код писал
`f.get("vcodec") or "none"` и превращал неизвестный в отсутствующий, из-за
чего у всех сайтов, не сообщающих кодеки (то есть почти у всех, кроме
YouTube), выбрасывались ВСЕ форматы.
"""
import pytest
import downloader as dl
class FakeYDL:
"""Подменяет YoutubeDL: отдаёт заранее заданные метаданные без сети."""
def __init__(self, info):
self._info = info
def __call__(self, opts):
return self
def __enter__(self):
return self
def __exit__(self, *a):
return False
def extract_info(self, url, download=False):
return self._info
def sanitize_info(self, info):
return info
@pytest.fixture
def probe_with(monkeypatch):
def run(info):
monkeypatch.setattr(dl.yt_dlp, "YoutubeDL", FakeYDL(info))
return dl.probe("https://example.com/v")
return run
def test_formats_with_unknown_codecs_are_kept(probe_with, fake_info):
out = probe_with(fake_info)
ids = [f["format_id"] for f in out["formats"]]
assert "hls-480" in ids, "формат с vcodec=None выброшен — это регрессия"
assert "mp4-low" in ids
def test_format_with_explicit_none_both_is_dropped(probe_with, fake_info):
"""Раскадровка не содержит ни видео, ни звука — её показывать незачем."""
out = probe_with(fake_info)
assert "sb0" not in [f["format_id"] for f in out["formats"]]
def test_kind_classification(probe_with, fake_info):
out = probe_with(fake_info)
kinds = {f["format_id"]: f["kind"] for f in out["formats"]}
assert kinds["137"] == "video" # есть видео, звука нет
assert kinds["140"] == "audio" # есть звук, видео нет
assert kinds["hls-480"] == "both" # неизвестно -> считаем полноценным
def test_unknown_codec_reported_as_none_not_crash(probe_with, fake_info):
"""Раньше здесь падал v.split('.') у None, и запрос отвечал общей
ошибкой вместо выдачи форматов."""
out = probe_with(fake_info)
f = next(f for f in out["formats"] if f["format_id"] == "hls-480")
assert f["vcodec"] is None and f["acodec"] is None
def test_codec_is_shortened(probe_with, fake_info):
out = probe_with(fake_info)
f = next(f for f in out["formats"] if f["format_id"] == "137")
assert f["vcodec"] == "avc1" # из 'avc1.640028'
def test_heights_include_unknown_codec_formats(probe_with, fake_info):
"""Список качеств строился по тому же принципу и терял те же форматы."""
out = probe_with(fake_info)
assert "480" in out["heights"]
assert "1080" in out["heights"]
def test_duration_limit(probe_with, fake_info, monkeypatch):
import config
monkeypatch.setattr(config, "MAX_DURATION_SEC", 60)
with pytest.raises(ValueError) as e:
probe_with(fake_info) # ролик на 120 секунд
assert str(e.value) == "too_long"
def test_empty_formats_does_not_crash(probe_with):
out = probe_with({"id": "x", "title": "Пусто", "formats": []})
assert out["formats"] == []
assert out["heights"] == []
def test_missing_title_gets_placeholder(probe_with):
out = probe_with({"id": "x", "formats": []})
assert out["title"] == "Без названия"