Rename test_ytdl_hardening.py to test_ytdl_hardening.py

This commit is contained in:
SteadyW 2026-06-21 11:29:30 +03:00 committed by GitHub
parent 9cc93a0a36
commit cca6f170a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,80 @@
"""
Hardening tests for ytdl: missing dependency, livestreams, atomic caching,
and audio-only sources. All but one are offline (yt-dlp/ffprobe are mocked).
"""
import shutil
import subprocess
from unittest import mock
import pytest
import ytdl
def _cp(stdout="", returncode=0, stderr=""):
return subprocess.CompletedProcess(args=[], returncode=returncode,
stdout=stdout, stderr=stderr)
def test_missing_ytdlp_gives_actionable_error(tmp_path):
with mock.patch("importlib.util.find_spec", return_value=None):
with pytest.raises(RuntimeError, match="pip install yt-dlp"):
ytdl.download("https://youtu.be/x", cache_dir=str(tmp_path))
def test_download_rejects_livestream(tmp_path):
# _probe_remote sees id on line 1, is_live=True on line 2.
with mock.patch("importlib.util.find_spec", return_value=object()), \
mock.patch.object(ytdl, "_ytdlp", return_value=_cp("vid123\nTrue\n")):
with pytest.raises(RuntimeError, match="live stream"):
ytdl.download("https://youtu.be/live", cache_dir=str(tmp_path))
def test_download_is_atomic_on_normalize_failure(tmp_path):
"""A failed normalize must leave no cache file a later run would trust."""
out = tmp_path / "vid123.mp4"
def fake_ytdlp(*args, **kwargs):
if "is_live" in args: # _probe_remote
return _cp("vid123\nFalse\n")
if "-o" in args: # the download itself
target = args[args.index("-o") + 1]
with open(target, "wb") as f: # simulate a downloaded file
f.write(b"\x00\x00")
return _cp("ok")
return _cp("")
with mock.patch("importlib.util.find_spec", return_value=object()), \
mock.patch.object(ytdl, "_ytdlp", side_effect=fake_ytdlp), \
mock.patch.object(ytdl, "normalize", side_effect=RuntimeError("boom")):
with pytest.raises(RuntimeError, match="boom"):
ytdl.download("https://youtu.be/x", cache_dir=str(tmp_path))
assert not out.exists() # no poisoned cache
assert not (tmp_path / "vid123.mp4.part.mp4").exists() # temp cleaned up
def test_cached_file_short_circuits_without_download(tmp_path):
out = tmp_path / "vid123.mp4"
out.write_bytes(b"already here")
def fake_ytdlp(*args, **kwargs):
if "is_live" in args:
return _cp("vid123\nFalse\n")
raise AssertionError("must not download when cached")
with mock.patch("importlib.util.find_spec", return_value=object()), \
mock.patch.object(ytdl, "_ytdlp", side_effect=fake_ytdlp):
assert ytdl.download("https://youtu.be/x", cache_dir=str(tmp_path)) == str(out)
@pytest.mark.skipif(not shutil.which("ffmpeg"), reason="ffmpeg required")
def test_normalize_rejects_audio_only(tmp_path):
audio = tmp_path / "audio_only.mp4"
r = subprocess.run(
["ffmpeg", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=1",
"-c:a", "aac", "-strict", "-2", "-loglevel", "error", str(audio)],
capture_output=True, text=True)
assert r.returncode == 0, r.stderr
with pytest.raises(RuntimeError, match="no video stream"):
ytdl.normalize(str(audio))