diff --git a/Dockerfile.test b/Dockerfile.test index 775be7d..869bc53 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -13,6 +13,7 @@ RUN apt-get update \ ocl-icd-libopencl1 \ pocl-opencl-icd \ p7zip-full \ + transmission-cli \ transmission-daemon \ && rm -rf /var/lib/apt/lists/* @@ -20,9 +21,11 @@ RUN python -m pip install -q uv==0.9.28 COPY . /workspace +ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 + RUN make install -ENV PATH="${HOME}/.local/bin:${PATH}" +ENV PATH="/workspace/.venv/bin:/root/.local/bin:${PATH}" ENV HATE_CRACK_SKIP_INIT=1 CMD ["bash", "-lc", "${HOME}/.local/bin/hate_crack --help >/tmp/hc_help.txt && ./hate_crack.py --help >/tmp/hc_script_help.txt"] diff --git a/hate_crack/api.py b/hate_crack/api.py index cdfa697..e80843e 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -259,7 +259,6 @@ class TransmissionSession: self.startup_timeout = startup_timeout self.shutdown_timeout = shutdown_timeout self._cfg_dir = "" - self._watch_dir = "" self._port = 0 self._rpc = "" self._proc = None @@ -270,8 +269,6 @@ class TransmissionSession: import subprocess self._cfg_dir = tempfile.mkdtemp(prefix="hate_crack_transmission_") - self._watch_dir = os.path.join(self._cfg_dir, "watch") - os.makedirs(self._watch_dir, exist_ok=True) self._port = _pick_free_port() self._rpc = f"127.0.0.1:{self._port}" self._proc = subprocess.Popen( @@ -288,8 +285,7 @@ class TransmissionSession: "--download-dir", self.save_dir, "--no-portmap", - "--watch-dir", - self._watch_dir, + "--no-watch-dir", ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, @@ -345,22 +341,38 @@ class TransmissionSession: return None def add(self, torrent_path: str) -> int: + import re + import subprocess + before_ids = {e["id"] for e in self.list()} - shutil.copy2(torrent_path, self._watch_dir) - deadline = time.monotonic() + 30.0 - while time.monotonic() < deadline: - after_entries = self.list() - new_ids = [e["id"] for e in after_entries if e["id"] not in before_ids] - if new_ids: - return new_ids[0] - time.sleep(0.5) + result = subprocess.run( + [ + "transmission-remote", + self._rpc, + "-a", + torrent_path, + ], + capture_output=True, + text=True, + ) + out = result.stdout or "" + m = re.search(r"Added torrent.*\n.*ID:\s*(\d+)", out) + if m: + return int(m.group(1)) + m = re.search(r"torrent added\s*\(id\s+(\d+)\)", out, re.IGNORECASE) + if m: + return int(m.group(1)) + after_entries = self.list() + new_ids = [e["id"] for e in after_entries if e["id"] not in before_ids] + if new_ids: + return new_ids[0] raise RuntimeError(f"Failed to add torrent: {torrent_path}") def list(self) -> list: import subprocess result = subprocess.run( - ["transmission-remote", self._rpc, "--no-auth", "-l"], + ["transmission-remote", self._rpc, "-l"], capture_output=True, text=True, ) @@ -417,7 +429,6 @@ class TransmissionSession: [ "transmission-remote", self._rpc, - "--no-auth", f"-t{torrent_id}", "--info-files", ], @@ -461,7 +472,6 @@ class TransmissionSession: [ "transmission-remote", self._rpc, - "--no-auth", f"-t{torrent_id}", "--remove", ], diff --git a/tests/test_api_downloads.py b/tests/test_api_downloads.py index cb21d7e..42d56bc 100644 --- a/tests/test_api_downloads.py +++ b/tests/test_api_downloads.py @@ -140,44 +140,41 @@ class TestTransmissionSession: with pytest.raises(RuntimeError, match="Transmission daemon failed"): ts.__enter__() - def test_add_copies_to_watch_dir_and_returns_new_id(self, tmp_path): + def test_add_uses_transmission_remote_and_returns_new_id(self, tmp_path): ts = TransmissionSession(str(tmp_path)) ts._rpc = "127.0.0.1:9999" - ts._watch_dir = str(tmp_path / "watch") - # Before: IDs 3 and 5. After first poll: ID 7 appears. + # Before: IDs 3 and 5. After add: ID 7 appears. list_calls = iter([ [{"id": 3}, {"id": 5}], [{"id": 3}, {"id": 5}, {"id": 7}], ]) - with patch("shutil.copy2"), patch.object(ts, "list", side_effect=list_calls), \ - patch("time.sleep"), patch("time.monotonic", side_effect=[0.0, 1.0]): + run_result = MagicMock(returncode=0, stdout="", stderr="") + with patch("subprocess.run", return_value=run_result), \ + patch.object(ts, "list", side_effect=list_calls): tid = ts.add("/tmp/foo.torrent") assert tid == 7 - def test_add_polls_until_daemon_picks_up_torrent(self, tmp_path): + def test_add_parses_id_from_output(self, tmp_path): ts = TransmissionSession(str(tmp_path)) ts._rpc = "127.0.0.1:9999" - ts._watch_dir = str(tmp_path / "watch") - # First two polls: no new ID. Third poll: ID 9 appears. - list_calls = iter([ - [{"id": 1}], - [{"id": 1}], - [{"id": 1}], - [{"id": 1}, {"id": 9}], - ]) - monotonic_vals = iter([0.0, 1.0, 2.0, 3.0, 4.0]) - with patch("shutil.copy2"), patch.object(ts, "list", side_effect=list_calls), \ - patch("time.sleep"), patch("time.monotonic", side_effect=monotonic_vals): + before_list = [{"id": 1}] + run_result = MagicMock( + returncode=0, + stdout="torrent added (id 42)\n", + stderr="", + ) + with patch("subprocess.run", return_value=run_result), \ + patch.object(ts, "list", return_value=before_list): tid = ts.add("/tmp/foo.torrent") - assert tid == 9 + assert tid == 42 - def test_add_raises_on_timeout(self, tmp_path): + def test_add_raises_when_torrent_not_added(self, tmp_path): ts = TransmissionSession(str(tmp_path)) ts._rpc = "127.0.0.1:9999" - ts._watch_dir = str(tmp_path / "watch") - # list never returns a new ID; monotonic jumps past the 10s deadline. - with patch("shutil.copy2"), patch.object(ts, "list", return_value=[{"id": 1}]), \ - patch("time.sleep"), patch("time.monotonic", side_effect=[0.0, 100.0]): + # list returns the same IDs before and after; output has no ID. + run_result = MagicMock(returncode=1, stdout="", stderr="error") + with patch("subprocess.run", return_value=run_result), \ + patch.object(ts, "list", return_value=[{"id": 1}]): with pytest.raises(RuntimeError): ts.add("/tmp/foo.torrent") diff --git a/tests/test_docker_script_install.py b/tests/test_docker_script_install.py index 1df1df7..4a26082 100644 --- a/tests/test_docker_script_install.py +++ b/tests/test_docker_script_install.py @@ -97,3 +97,45 @@ def test_docker_hashcat_cracks_simple_password(docker_image): assert run.returncode == 0, ( f"Docker hashcat crack failed. stdout={run.stdout} stderr={run.stderr}" ) + + +@pytest.mark.timeout(300) +def test_docker_torrent_downloads_wordlists(docker_image, tmp_path): + downloads_dir = tmp_path / "downloads" + downloads_dir.mkdir() + + py_cmd = ( + "from hate_crack.api import fetch_torrent_metadata, run_torrent_session; " + "t1 = fetch_torrent_metadata('ignis-10K.txt'); " + "t2 = fetch_torrent_metadata('hashmob.net_2025.micro.found'); " + "files = [f for f in (t1, t2) if f]; " + "run_torrent_session(files, '/downloads')" + ) + + try: + run = subprocess.run( + [ + "docker", "run", "--rm", + "-v", f"{downloads_dir}:/downloads", + docker_image, + "bash", "-lc", f"/workspace/.venv/bin/python -c \"{py_cmd}\"", + ], + capture_output=True, + text=True, + timeout=300, + ) + except subprocess.TimeoutExpired as exc: + pytest.fail(f"Docker torrent test timed out after {exc.timeout}s") + + assert run.returncode == 0, ( + f"Torrent session failed. stdout={run.stdout} stderr={run.stderr}" + ) + + ignis10k = downloads_dir / "ignis-10K.txt" + micro = downloads_dir / "hashmob.net_2025.micro.found" + assert ignis10k.exists() and ignis10k.stat().st_size > 0, ( + f"ignis-10K.txt missing/empty. stdout={run.stdout} stderr={run.stderr}" + ) + assert micro.exists() and micro.stat().st_size > 0, ( + f"hashmob.net_2025.micro.found missing/empty. stdout={run.stdout} stderr={run.stderr}" + )