#!/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:]]))