"""Tests for API tools - Refactored to use real components with minimal mocking.""" from unittest.mock import Mock, patch from agent.tools import api as api_tools from infrastructure.persistence import get_memory def create_mock_response(status_code, json_data=None, text=None): """Helper to create properly mocked HTTP response.""" response = Mock() response.status_code = status_code response.raise_for_status = Mock() if json_data is not None: response.json = Mock(return_value=json_data) if text is not None: response.text = text return response class TestFindMediaImdbId: """Tests for find_media_imdb_id tool.""" @patch("infrastructure.api.tmdb.client.requests.get") def test_success(self, mock_get, memory): """Should return movie info on success.""" # Mock HTTP responses def mock_get_side_effect(url, **kwargs): if "search" in url: return create_mock_response( 200, json_data={ "results": [ { "id": 27205, "title": "Inception", "release_date": "2010-07-16", "overview": "A thief...", "media_type": "movie", } ] }, ) elif "external_ids" in url: return create_mock_response(200, json_data={"imdb_id": "tt1375666"}) mock_get.side_effect = mock_get_side_effect result = api_tools.find_media_imdb_id("Inception") assert result["status"] == "ok" assert result["imdb_id"] == "tt1375666" assert result["title"] == "Inception" # Verify HTTP calls assert mock_get.call_count == 2 @patch("infrastructure.api.tmdb.client.requests.get") def test_stores_in_stm(self, mock_get, memory): """Should store result in STM on success.""" def mock_get_side_effect(url, **kwargs): if "search" in url: return create_mock_response( 200, json_data={ "results": [ { "id": 27205, "title": "Inception", "release_date": "2010-07-16", "media_type": "movie", } ] }, ) elif "external_ids" in url: return create_mock_response(200, json_data={"imdb_id": "tt1375666"}) mock_get.side_effect = mock_get_side_effect api_tools.find_media_imdb_id("Inception") mem = get_memory() entity = mem.stm.get_entity("last_media_search") assert entity is not None assert entity["title"] == "Inception" assert mem.stm.current_topic == "searching_media" @patch("infrastructure.api.tmdb.client.requests.get") def test_not_found(self, mock_get, memory): """Should return error when not found.""" mock_get.return_value = create_mock_response(200, json_data={"results": []}) result = api_tools.find_media_imdb_id("NonexistentMovie12345") assert result["status"] == "error" assert result["error"] == "not_found" @patch("infrastructure.api.tmdb.client.requests.get") def test_does_not_store_on_error(self, mock_get, memory): """Should not store in STM on error.""" mock_get.return_value = create_mock_response(200, json_data={"results": []}) api_tools.find_media_imdb_id("Test") mem = get_memory() assert mem.stm.get_entity("last_media_search") is None class TestFindTorrent: """Tests for find_torrent tool.""" @patch("infrastructure.api.knaben.client.requests.post") def test_success(self, mock_post, memory): """Should return torrents on success.""" mock_post.return_value = create_mock_response( 200, json_data={ "hits": [ { "title": "Torrent 1", "seeders": 100, "leechers": 10, "magnetUrl": "magnet:?xt=...", "size": "2.5 GB", }, { "title": "Torrent 2", "seeders": 50, "leechers": 5, "magnetUrl": "magnet:?xt=...", "size": "1.8 GB", }, ] }, ) result = api_tools.find_torrent("Inception 1080p") assert result["status"] == "ok" assert len(result["torrents"]) == 2 # Verify HTTP payload payload = mock_post.call_args[1]["json"] assert payload["query"] == "Inception 1080p" @patch("infrastructure.api.knaben.client.requests.post") def test_stores_in_episodic(self, mock_post, memory): """Should store results in episodic memory.""" mock_post.return_value = create_mock_response( 200, json_data={ "hits": [ { "title": "Torrent 1", "seeders": 100, "leechers": 10, "magnetUrl": "magnet:?xt=...", "size": "2.5 GB", } ] }, ) api_tools.find_torrent("Inception") mem = get_memory() assert mem.episodic.last_search_results is not None assert mem.episodic.last_search_results["query"] == "Inception" assert mem.stm.current_topic == "selecting_torrent" @patch("infrastructure.api.knaben.client.requests.post") def test_results_have_indexes(self, mock_post, memory): """Should add indexes to results.""" mock_post.return_value = create_mock_response( 200, json_data={ "hits": [ { "title": "Torrent 1", "seeders": 100, "leechers": 10, "magnetUrl": "magnet:?xt=1", "size": "1GB", }, { "title": "Torrent 2", "seeders": 50, "leechers": 5, "magnetUrl": "magnet:?xt=2", "size": "2GB", }, { "title": "Torrent 3", "seeders": 25, "leechers": 2, "magnetUrl": "magnet:?xt=3", "size": "3GB", }, ] }, ) api_tools.find_torrent("Test") mem = get_memory() results = mem.episodic.last_search_results["results"] assert results[0]["index"] == 1 assert results[1]["index"] == 2 assert results[2]["index"] == 3 @patch("infrastructure.api.knaben.client.requests.post") def test_not_found(self, mock_post, memory): """Should return error when no torrents found.""" mock_post.return_value = create_mock_response(200, json_data={"hits": []}) result = api_tools.find_torrent("NonexistentMovie12345") assert result["status"] == "error" class TestGetTorrentByIndex: """Tests for get_torrent_by_index tool.""" def test_success(self, memory_with_search_results): """Should return torrent at index.""" result = api_tools.get_torrent_by_index(2) assert result["status"] == "ok" assert result["torrent"]["name"] == "Inception.2010.1080p.WEB-DL.x265" def test_first_index(self, memory_with_search_results): """Should return first torrent.""" result = api_tools.get_torrent_by_index(1) assert result["status"] == "ok" assert result["torrent"]["name"] == "Inception.2010.1080p.BluRay.x264" def test_last_index(self, memory_with_search_results): """Should return last torrent.""" result = api_tools.get_torrent_by_index(3) assert result["status"] == "ok" assert result["torrent"]["name"] == "Inception.2010.720p.BluRay" def test_index_out_of_range(self, memory_with_search_results): """Should return error for invalid index.""" result = api_tools.get_torrent_by_index(10) assert result["status"] == "error" assert result["error"] == "not_found" def test_index_zero(self, memory_with_search_results): """Should return error for index 0.""" result = api_tools.get_torrent_by_index(0) assert result["status"] == "error" assert result["error"] == "not_found" def test_negative_index(self, memory_with_search_results): """Should return error for negative index.""" result = api_tools.get_torrent_by_index(-1) assert result["status"] == "error" assert result["error"] == "not_found" def test_no_search_results(self, memory): """Should return error if no search results.""" result = api_tools.get_torrent_by_index(1) assert result["status"] == "error" assert result["error"] == "not_found" assert "Search for torrents first" in result["message"] class TestAddTorrentToQbittorrent: """Tests for add_torrent_to_qbittorrent tool. Note: These tests mock the qBittorrent client because: 1. The client requires authentication/session management 2. We want to test the tool's logic (memory updates, workflow management) 3. The client itself is tested separately in infrastructure tests This is acceptable mocking because we're testing the TOOL logic, not the client. """ @patch("agent.tools.api.qbittorrent_client") def test_success(self, mock_client, memory): """Should add torrent successfully and update memory.""" mock_client.add_torrent.return_value = True result = api_tools.add_torrent_to_qbittorrent("magnet:?xt=urn:btih:abc123") # Test tool logic assert result["status"] == "ok" # Verify client was called correctly mock_client.add_torrent.assert_called_once_with("magnet:?xt=urn:btih:abc123") @patch("agent.tools.api.qbittorrent_client") def test_adds_to_active_downloads(self, mock_client, memory_with_search_results): """Should add to active downloads on success.""" mock_client.add_torrent.return_value = True api_tools.add_torrent_to_qbittorrent("magnet:?xt=urn:btih:abc123") # Test memory update logic mem = get_memory() assert len(mem.episodic.active_downloads) == 1 assert ( mem.episodic.active_downloads[0]["name"] == "Inception.2010.1080p.BluRay.x264" ) @patch("agent.tools.api.qbittorrent_client") def test_sets_topic_and_ends_workflow(self, mock_client, memory): """Should set topic and end workflow.""" mock_client.add_torrent.return_value = True memory.stm.start_workflow("download", {"title": "Test"}) api_tools.add_torrent_to_qbittorrent("magnet:?xt=...") # Test workflow management logic mem = get_memory() assert mem.stm.current_topic == "downloading" assert mem.stm.current_workflow is None @patch("agent.tools.api.qbittorrent_client") def test_error_handling(self, mock_client, memory): """Should handle client errors correctly.""" from infrastructure.api.qbittorrent.exceptions import QBittorrentAPIError mock_client.add_torrent.side_effect = QBittorrentAPIError("Connection failed") result = api_tools.add_torrent_to_qbittorrent("magnet:?xt=...") # Test error handling logic assert result["status"] == "error" class TestAddTorrentByIndex: """Tests for add_torrent_by_index tool. These tests verify the tool's logic: - Getting torrent from memory by index - Extracting magnet link - Calling add_torrent_to_qbittorrent - Error handling for edge cases """ @patch("agent.tools.api.qbittorrent_client") def test_success(self, mock_client, memory_with_search_results): """Should get torrent by index and add it.""" mock_client.add_torrent.return_value = True result = api_tools.add_torrent_by_index(1) # Test tool logic assert result["status"] == "ok" assert result["torrent_name"] == "Inception.2010.1080p.BluRay.x264" # Verify correct magnet was extracted and used mock_client.add_torrent.assert_called_once_with("magnet:?xt=urn:btih:abc123") @patch("agent.tools.api.qbittorrent_client") def test_uses_correct_magnet(self, mock_client, memory_with_search_results): """Should extract correct magnet from index.""" mock_client.add_torrent.return_value = True api_tools.add_torrent_by_index(2) # Test magnet extraction logic mock_client.add_torrent.assert_called_once_with("magnet:?xt=urn:btih:def456") def test_invalid_index(self, memory_with_search_results): """Should return error for invalid index.""" result = api_tools.add_torrent_by_index(99) # Test error handling logic (no mock needed) assert result["status"] == "error" assert result["error"] == "not_found" def test_no_search_results(self, memory): """Should return error if no search results.""" result = api_tools.add_torrent_by_index(1) # Test error handling logic (no mock needed) assert result["status"] == "error" assert result["error"] == "not_found" def test_no_magnet_link(self, memory): """Should return error if torrent has no magnet.""" memory.episodic.store_search_results( "test", [{"name": "Torrent without magnet", "seeders": 100}], ) result = api_tools.add_torrent_by_index(1) # Test error handling logic (no mock needed) assert result["status"] == "error" assert result["error"] == "no_magnet"