Files
playground/torrentify_test.py
T

248 lines
11 KiB
Python

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["click", "httpx", "torf", "pytest"]
# ///
import base64
import hashlib
import json
from pathlib import Path
import sys
from unittest.mock import patch
from click.testing import CliRunner
import httpx
import pytest
import torf
import torrentify as script
@pytest.mark.parametrize("kind", ["file", "directory", "mixed"])
def test_create_selected_content(tmp_path: Path, kind: str) -> None:
source = tmp_path / "first.txt"
source.write_bytes(b"first")
directory = tmp_path / "folder"
directory.mkdir()
(directory / "nested.txt").write_bytes(b"nested")
(tmp_path / "unselected.txt").write_bytes(b"unselected")
paths = {"file": (source,), "directory": (directory,), "mixed": (source, directory)}[kind]
output = tmp_path / "result.torrent"
result = CliRunner().invoke(cli=script.main, args=["create", *map(str, paths), "--output", str(output)])
assert result.exit_code == 0, result.output
torrent = torf.Torrent.read(filepath=output)
assert torrent.private is True
assert torrent.created_by == script.AUTHOR
assert list(torrent.trackers.flat) == list(script.ANNOUNCE_URLS)
expected = {"file": b"first", "directory": b"nested", "mixed": b"firstnested"}[kind]
assert torrent.metainfo["info"]["pieces"] == hashlib.sha1(expected).digest()
if kind == "mixed":
assert [item["path"] for item in torrent.metainfo["info"]["files"]] == [
["first.txt"], ["folder", "nested.txt"],
]
if kind == "directory":
assert torrent.name == "folder"
assert torrent.metainfo["info"]["files"][0]["path"] == ["nested.txt"]
def test_reject_inputs_in_different_directories(tmp_path: Path) -> None:
source = tmp_path / "first.txt"
source.write_bytes(b"first")
directory = tmp_path / "folder"
directory.mkdir()
nested = directory / "nested.txt"
nested.write_bytes(b"nested")
result = CliRunner().invoke(cli=script.main, args=["create", str(source), str(nested)])
assert result.exit_code == 1
assert "All inputs must be in the same directory" in result.output
assert not list(tmp_path.glob("*.torrent"))
def test_create_rejects_empty_and_existing_output(tmp_path: Path) -> None:
source = tmp_path / "file.txt"
source.touch()
with pytest.raises(ValueError, match="no nonempty files"):
script.create_torrent(paths=(source,), output=tmp_path / "result.torrent")
source.write_bytes(b"content")
output = script.create_torrent(paths=(source,), output=tmp_path / "result.torrent")
original = output.read_bytes()
with pytest.raises(FileExistsError):
script.create_torrent(paths=(source,), output=output)
assert output.read_bytes() == original
with pytest.raises(ValueError, match="overwrite a source"):
script.create_torrent(paths=(source,), output=source, overwrite=True)
assert source.read_bytes() == b"content"
def test_modify_metadata_without_content(tmp_path: Path) -> None:
source = tmp_path / "content.txt"
source.write_bytes(b"content")
output = script.create_torrent(paths=(source,), output=tmp_path / "original.torrent")
before = torf.Torrent.read(filepath=output)
source.unlink()
result = CliRunner().invoke(cli=script.main, args=[
"modify", str(output), "--announce-url", "https://tracker.example/announce",
"--announce-url", "udp://tracker.example:80/announce", "--author", "new author",
"--name", "new name", "--no-private",
])
assert result.exit_code == 0, result.output
after = torf.Torrent.read(filepath=output)
assert after.name == "new name"
assert after.created_by == "new author"
assert after.private is False
assert list(after.trackers.flat) == ["https://tracker.example/announce", "udp://tracker.example:80/announce"]
assert after.metainfo["info"]["pieces"] == before.metainfo["info"]["pieces"]
def test_modify_preserves_omitted_fields_and_original(tmp_path: Path) -> None:
source = tmp_path / "content.txt"
source.write_bytes(b"content")
original = script.create_torrent(paths=(source,), output=tmp_path / "original.torrent")
original_bytes = original.read_bytes()
output = tmp_path / "modified.torrent"
script.modify_torrent(torrent_path=original, output=output, author="replacement")
before = torf.Torrent.read(filepath=original)
after = torf.Torrent.read(filepath=output)
assert original.read_bytes() == original_bytes
assert after.infohash == before.infohash
assert after.private == before.private
assert after.trackers == before.trackers
assert after.name == before.name
with pytest.raises(torf.WriteError):
script.modify_torrent(torrent_path=original, output=output)
@pytest.mark.parametrize("added_key", ["torrent-added", "torrent-duplicate"])
@pytest.mark.parametrize("url", ["http://transmission:9091", "http://transmission:9091/transmission/rpc"])
def test_enqueue_negotiates_session_and_sets_remote_directory(tmp_path: Path, added_key: str, url: str) -> None:
torrent_path = tmp_path / "content.torrent"
torrent_path.write_bytes(b"torrent data")
requests = []
def respond(request: httpx.Request) -> httpx.Response:
requests.append(request)
if len(requests) == 1:
return httpx.Response(status_code=409, headers={"X-Transmission-Session-Id": "session-token"})
return httpx.Response(status_code=200, json={
"result": "success", "arguments": {added_key: {"hashString": "torrent-hash"}},
})
client = httpx.Client(transport=httpx.MockTransport(handler=respond), auth=httpx.BasicAuth("user", "pass:word"))
with patch.object(script.httpx, "Client", return_value=client) as factory:
result = script.enqueue_torrent(
transmission_url=url, credentials="user:pass:word",
torrent_path=torrent_path, download_path="/remote/downloads",
)
assert result == "torrent-hash"
assert factory.call_args.kwargs["auth"]._auth_header == client.auth._auth_header
assert len(requests) == 2
assert requests[1].headers["X-Transmission-Session-Id"] == "session-token"
assert str(requests[1].url) == "http://transmission:9091/transmission/rpc"
payload = json.loads(requests[1].content)
assert payload["method"] == "torrent-add"
assert payload["arguments"]["download-dir"] == "/remote/downloads"
assert payload["arguments"]["paused"] is False
assert base64.b64decode(payload["arguments"]["metainfo"]) == torrent_path.read_bytes()
@pytest.mark.parametrize("status, result", [(401, "success"), (200, "invalid torrent"), (409, "success")])
def test_enqueue_reports_failures(tmp_path: Path, status: int, result: str) -> None:
torrent_path = tmp_path / "content.torrent"
torrent_path.write_bytes(b"torrent data")
def respond(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=status, headers={"X-Transmission-Session-Id": "token"}, json={"result": result})
client = httpx.Client(transport=httpx.MockTransport(handler=respond))
with patch.object(script.httpx, "Client", return_value=client):
with pytest.raises((httpx.HTTPStatusError, ValueError)):
script.enqueue_torrent(
transmission_url="http://transmission:9091", credentials=None,
torrent_path=torrent_path, download_path="/remote/downloads",
)
@pytest.mark.parametrize("use_environment", [False, True])
def test_enqueue_command(tmp_path: Path, use_environment: bool) -> None:
torrent_path = tmp_path / "content.torrent"
torrent_path.write_bytes(b"torrent data")
arguments = ["enqueue", str(torrent_path), "--download-path", "/remote/downloads"]
environment = {"TRANSMISSION_HOST": "http://transmission:9091", "TRANSMISSION_AUTH": "user:password"}
if not use_environment:
arguments.extend(["--transmission-url", environment["TRANSMISSION_HOST"], "--credentials", environment["TRANSMISSION_AUTH"]])
with patch.object(script, "enqueue_torrent", return_value="torrent-hash") as enqueue:
result = CliRunner().invoke(cli=script.main, args=arguments, env=environment if use_environment else {})
assert result.exit_code == 0, result.output
enqueue.assert_called_once_with(
transmission_url="http://transmission:9091", credentials="user:password",
torrent_path=torrent_path, download_path=Path("/remote/downloads"),
)
def test_enqueue_command_reports_error(tmp_path: Path) -> None:
torrent_path = tmp_path / "content.torrent"
torrent_path.write_bytes(b"torrent data")
with patch.object(script, "enqueue_torrent", side_effect=ValueError("Transmission rejected torrent")):
result = CliRunner().invoke(cli=script.main, args=[
"enqueue", str(torrent_path), "--host", "http://transmission:9091",
"--download-path", "/remote/downloads",
])
assert result.exit_code == 1
assert "Error: Transmission rejected torrent" in result.output
@pytest.mark.parametrize("directory", [False, True])
@pytest.mark.parametrize("json_output", [False, True])
def test_inspect_reports_properties_and_files_without_content(
tmp_path: Path, directory: bool, json_output: bool,
) -> None:
folder = tmp_path / "content"
folder.mkdir()
source = folder / "first.txt"
source.write_bytes(b"first")
if directory:
(folder / "second.txt").write_bytes(b"second")
output = script.create_torrent(paths=(folder if directory else source,), output=tmp_path / "content.torrent")
original = output.read_bytes()
source.unlink()
arguments = ["inspect", str(output)]
if json_output:
arguments.append("--json")
result = CliRunner().invoke(cli=script.main, args=arguments)
assert result.exit_code == 0, result.output
assert result.stderr == ""
if json_output:
report = json.loads(result.stdout)
assert report["properties"]["author"] == script.AUTHOR
assert report["properties"]["private"] is True
assert report["properties"]["announce_urls"] == [list(script.ANNOUNCE_URLS)]
assert report["properties"]["size"] == (11 if directory else 5)
assert report["files"] == (
[{"path": "content/first.txt", "size": 5}, {"path": "content/second.txt", "size": 6}]
if directory else [{"path": "first.txt", "size": 5}]
)
else:
assert f"Author: {script.AUTHOR}\n" in result.stdout
assert "Private: yes\n" in result.stdout
assert f"Size: {11 if directory else 5} bytes\n" in result.stdout
assert f"Tier 1: {script.ANNOUNCE_URLS[0]}\n" in result.stdout
assert f"Files ({2 if directory else 1}):\n" in result.stdout
assert f" {'content/' if directory else ''}first.txt: 5 bytes\n" in result.stdout
if directory:
assert " content/second.txt: 6 bytes\n" in result.stdout
assert output.read_bytes() == original
def test_inspect_rejects_invalid_torrent(tmp_path: Path) -> None:
torrent_path = tmp_path / "invalid.torrent"
torrent_path.write_bytes(b"invalid")
result = CliRunner().invoke(cli=script.main, args=["inspect", str(torrent_path)])
assert result.exit_code == 1
assert "Error:" in result.output
if __name__ == "__main__":
sys.exit(pytest.main(args=[str(Path(__file__).resolve()), *sys.argv[1:]]))