"""Tests for ``alfred.application.torrents.add_torrent.AddTorrentUseCase``. Wraps ``QBittorrentClient.add_torrent`` with magnet-link validation and exception translation into an ``AddTorrentResponse`` envelope. Coverage: - ``TestValidation`` — empty / non-string / non-magnet rejection. - ``TestSuccess`` — client returns True → status="ok". - ``TestAddFailure`` — client returns False → status="error", error="add_failed". - ``TestErrorTranslation`` — ``QBittorrentAuthError`` → authentication_failed, ``QBittorrentAPIError`` → api_error. QBittorrentClient is fully mocked. """ from __future__ import annotations from unittest.mock import MagicMock import pytest from alfred.application.torrents.add_torrent import AddTorrentUseCase from alfred.infrastructure.api.qbittorrent.exceptions import ( QBittorrentAPIError, QBittorrentAuthError, ) @pytest.fixture def client(): return MagicMock() @pytest.fixture def use_case(client): return AddTorrentUseCase(client) VALID_MAGNET = "magnet:?xt=urn:btih:abc" # --------------------------------------------------------------------------- # # Validation # # --------------------------------------------------------------------------- # class TestValidation: @pytest.mark.parametrize("bad", ["", None, 42, b"magnet:?x"]) def test_invalid_inputs_return_validation_failed(self, use_case, client, bad): r = use_case.execute(bad) assert r.status == "error" assert r.error == "validation_failed" client.add_torrent.assert_not_called() def test_non_magnet_scheme_rejected(self, use_case, client): r = use_case.execute("http://example.com/torrent") assert r.status == "error" assert r.error == "validation_failed" assert "magnet" in r.message.lower() client.add_torrent.assert_not_called() # --------------------------------------------------------------------------- # # Success # # --------------------------------------------------------------------------- # class TestSuccess: def test_add_success(self, use_case, client): client.add_torrent.return_value = True r = use_case.execute(VALID_MAGNET) assert r.status == "ok" assert r.error is None assert "success" in r.message.lower() client.add_torrent.assert_called_once_with(VALID_MAGNET) # --------------------------------------------------------------------------- # # Add failure # # --------------------------------------------------------------------------- # class TestAddFailure: def test_add_returns_false(self, use_case, client): client.add_torrent.return_value = False r = use_case.execute(VALID_MAGNET) assert r.status == "error" assert r.error == "add_failed" # --------------------------------------------------------------------------- # # Error translation # # --------------------------------------------------------------------------- # class TestErrorTranslation: def test_auth_error_translated(self, use_case, client): client.add_torrent.side_effect = QBittorrentAuthError("bad creds") r = use_case.execute(VALID_MAGNET) assert r.status == "error" assert r.error == "authentication_failed" # The message is a fixed user-facing string, not the raw exception. assert "authenticate" in r.message.lower() def test_api_error_translated(self, use_case, client): client.add_torrent.side_effect = QBittorrentAPIError("server down") r = use_case.execute(VALID_MAGNET) assert r.status == "error" assert r.error == "api_error" assert "server down" in r.message