"""Test cases for YouTube MCP components."""
import pytest
from unittest.mock import Mock, patch, MagicMock
from youtube_mcp.browser_controller import BrowserController
from youtube_mcp.youtube_client import YouTubeClient
from youtube_mcp.server import YouTubeMCPServer
class TestBrowserController:
"""Test suite for BrowserController."""
def test_get_system_returns_valid_os(self) -> None:
"""Test that get_system returns a valid OS string."""
system = BrowserController.get_system()
assert system in ["windows", "darwin", "linux", "wsl"]
@patch("platform.system")
def test_get_system_windows(self, mock_system: Mock) -> None:
"""Test Windows detection."""
mock_system.return_value = "Windows"
assert BrowserController.get_system() == "windows"
@patch("platform.system")
def test_get_system_macos(self, mock_system: Mock) -> None:
"""Test macOS detection."""
mock_system.return_value = "Darwin"
assert BrowserController.get_system() == "darwin"
@patch("builtins.open", create=True)
@patch("platform.system")
def test_get_system_linux(self, mock_system: Mock, mock_open: Mock) -> None:
"""Test Linux detection (non-WSL)."""
mock_system.return_value = "Linux"
# Simulate non-WSL Linux by raising exception on /proc/version read
mock_open.side_effect = FileNotFoundError()
assert BrowserController.get_system() == "linux"
@patch("builtins.open", create=True)
@patch("platform.system")
def test_get_system_wsl_detection(self, mock_system: Mock, mock_open: Mock) -> None:
"""Test WSL detection."""
mock_system.return_value = "Linux"
mock_open.return_value.__enter__.return_value.read.return_value = "microsoft wsl"
result = BrowserController.get_system()
assert result == "wsl"
def test_init_sets_current_process_id_to_none(self) -> None:
"""Test that BrowserController initializes with current_process_id as None."""
controller = BrowserController()
assert controller.current_process_id is None
@patch("subprocess.Popen")
@patch.object(BrowserController, "get_system")
def test_open_youtube_url_returns_success_dict(
self, mock_get_system: Mock, mock_popen: Mock
) -> None:
"""Test that open_youtube_url returns a success dictionary."""
mock_get_system.return_value = "linux"
mock_process = MagicMock()
mock_process.pid = 12345
mock_popen.return_value = mock_process
controller = BrowserController()
result = controller.open_youtube_url("https://www.youtube.com/watch?v=test123")
assert isinstance(result, dict)
assert result["success"] is True
assert "message" in result
assert "system" in result
assert "url" in result
@patch("subprocess.Popen")
@patch.object(BrowserController, "get_system")
def test_open_youtube_url_sets_process_id(
self, mock_get_system: Mock, mock_popen: Mock
) -> None:
"""Test that open_youtube_url sets the current_process_id."""
mock_get_system.return_value = "linux"
mock_process = MagicMock()
mock_process.pid = 12345
mock_popen.return_value = mock_process
controller = BrowserController()
controller.open_youtube_url("https://www.youtube.com/watch?v=test123")
assert controller.current_process_id == 12345
@patch.object(BrowserController, "_close_previous_browser")
@patch("subprocess.Popen")
@patch.object(BrowserController, "get_system")
def test_open_youtube_url_calls_close_previous(
self, mock_get_system: Mock, mock_popen: Mock, mock_close: Mock
) -> None:
"""Test that open_youtube_url closes previous browser."""
mock_get_system.return_value = "linux"
mock_process = MagicMock()
mock_process.pid = 12345
mock_popen.return_value = mock_process
controller = BrowserController()
controller.open_youtube_url("https://www.youtube.com/watch?v=test123")
mock_close.assert_called_once()
class TestYouTubeClient:
"""Test suite for YouTubeClient."""
@patch("youtube_mcp.youtube_client.build")
def test_init_creates_youtube_client(self, mock_build: Mock) -> None:
"""Test that YouTubeClient initializes with API key."""
mock_build.return_value = MagicMock()
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough" # 37 chars
client = YouTubeClient(valid_api_key)
mock_build.assert_called_once_with(
"youtube", "v3", developerKey=valid_api_key
)
assert client.youtube is not None
@patch("youtube_mcp.youtube_client.build")
def test_init_raises_on_invalid_api_key(self, mock_build: Mock) -> None:
"""Test that YouTubeClient raises ValueError on invalid API key."""
with pytest.raises(ValueError, match="API key must be a non-empty string"):
YouTubeClient("")
@patch("youtube_mcp.youtube_client.build")
def test_search_song_returns_list(self, mock_build: Mock) -> None:
"""Test that search_song returns a list of results."""
mock_youtube = MagicMock()
mock_build.return_value = mock_youtube
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
# Mock the API response
mock_request = MagicMock()
mock_youtube.search().list.return_value = mock_request
mock_request.execute.return_value = {
"items": [
{
"id": {"videoId": "test123"},
"snippet": {
"title": "Test Song",
"channelTitle": "Test Channel",
},
}
]
}
client = YouTubeClient(valid_api_key)
results = client.search_song("test query", max_results=5)
assert isinstance(results, list)
assert len(results) == 1
assert results[0]["title"] == "Test Song"
assert results[0]["channel"] == "Test Channel"
assert results[0]["video_id"] == "test123"
assert "url" in results[0]
@patch("youtube_mcp.youtube_client.build")
def test_search_song_uses_region_code(self, mock_build: Mock) -> None:
"""Test that search_song uses the specified region code."""
mock_youtube = MagicMock()
mock_build.return_value = mock_youtube
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_request = MagicMock()
mock_youtube.search().list.return_value = mock_request
mock_request.execute.return_value = {"items": []}
client = YouTubeClient(valid_api_key)
client.search_song("test", region_code="US")
mock_youtube.search().list.assert_called_once()
call_kwargs = mock_youtube.search().list.call_args[1]
assert call_kwargs["regionCode"] == "US"
@patch("youtube_mcp.youtube_client.build")
def test_search_song_raises_on_api_error(self, mock_build: Mock) -> None:
"""Test that search_song raises RuntimeError on API error."""
mock_youtube = MagicMock()
mock_build.return_value = mock_youtube
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_request = MagicMock()
mock_youtube.search().list.return_value = mock_request
mock_request.execute.side_effect = Exception("API Error")
client = YouTubeClient(valid_api_key)
with pytest.raises(RuntimeError, match="Error searching YouTube"):
client.search_song("test")
def test_search_song_validates_query(self) -> None:
"""Test that search_song validates query parameter."""
with patch("youtube_mcp.youtube_client.build"):
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
client = YouTubeClient(valid_api_key)
with pytest.raises(ValueError, match="Query must be a non-empty string"):
client.search_song("")
def test_search_song_validates_max_results(self) -> None:
"""Test that search_song validates max_results parameter."""
with patch("youtube_mcp.youtube_client.build"):
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
client = YouTubeClient(valid_api_key)
with pytest.raises(ValueError, match="max_results must be between 1 and 50"):
client.search_song("test", max_results=100)
class TestYouTubeMCPServer:
"""Test suite for YouTubeMCPServer."""
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_init_creates_clients(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that YouTubeMCPServer initializes clients."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
server = YouTubeMCPServer(valid_api_key)
mock_youtube.assert_called_once_with(valid_api_key)
mock_browser.assert_called_once()
assert server.youtube_client is not None
assert server.browser_controller is not None
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_get_tools_returns_list(self, mock_browser: Mock, mock_youtube: Mock) -> None:
"""Test that get_tools returns a list of tools."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
server = YouTubeMCPServer(valid_api_key)
tools = server.get_tools()
assert isinstance(tools, list)
assert len(tools) == 2
tool_names = [tool["name"] for tool in tools]
assert "search_youtube" in tool_names
assert "play_youtube" in tool_names
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_get_tools_search_youtube_has_correct_schema(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that search_youtube tool has correct input schema."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
server = YouTubeMCPServer(valid_api_key)
tools = server.get_tools()
search_tool = [t for t in tools if t["name"] == "search_youtube"][0]
assert "description" in search_tool
assert "inputSchema" in search_tool
assert search_tool["inputSchema"]["properties"]["query"]["type"] == "string"
assert "query" in search_tool["inputSchema"]["required"]
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_get_tools_play_youtube_has_correct_schema(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that play_youtube tool has correct input schema."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
server = YouTubeMCPServer(valid_api_key)
tools = server.get_tools()
play_tool = [t for t in tools if t["name"] == "play_youtube"][0]
assert "description" in play_tool
assert "inputSchema" in play_tool
assert play_tool["inputSchema"]["properties"]["url"]["type"] == "string"
assert "url" in play_tool["inputSchema"]["required"]
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_handle_search_youtube_returns_success(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that handle_search_youtube returns success response."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_client_instance = MagicMock()
mock_youtube.return_value = mock_client_instance
mock_client_instance.search_song.return_value = [
{
"title": "Test Song",
"channel": "Test Channel",
"video_id": "test123",
"url": "https://www.youtube.com/watch?v=test123",
}
]
server = YouTubeMCPServer(valid_api_key)
result = server.handle_search_youtube("test query")
assert result["success"] is True
assert "results" in result
assert "count" in result
assert result["count"] == 1
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_handle_search_youtube_returns_error(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that handle_search_youtube returns error on exception."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_client_instance = MagicMock()
mock_youtube.return_value = mock_client_instance
mock_client_instance.search_song.side_effect = Exception("Test error")
server = YouTubeMCPServer(valid_api_key)
result = server.handle_search_youtube("test query")
assert result["success"] is False
assert "error" in result
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_handle_play_youtube_returns_success(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that handle_play_youtube returns success response."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_browser_instance = MagicMock()
mock_browser.return_value = mock_browser_instance
mock_browser_instance.open_youtube_url.return_value = {
"success": True,
"message": "Song is now playing in your browser",
"system": "linux",
"url": "https://www.youtube.com/watch?v=test123",
}
server = YouTubeMCPServer(valid_api_key)
result = server.handle_play_youtube("https://www.youtube.com/watch?v=test123")
assert result["success"] is True
assert "message" in result
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_handle_play_youtube_returns_error(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that handle_play_youtube returns error on exception."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_browser_instance = MagicMock()
mock_browser.return_value = mock_browser_instance
mock_browser_instance.open_youtube_url.side_effect = Exception("Test error")
server = YouTubeMCPServer(valid_api_key)
result = server.handle_play_youtube("https://www.youtube.com/watch?v=test123")
assert result["success"] is False
assert "error" in result
class TestValidators:
"""Test suite for validator functions."""
def test_validate_youtube_url_valid_simple_video(self) -> None:
"""Test validation of simple YouTube video URL."""
from youtube_mcp.validators import validate_youtube_url
is_valid, message = validate_youtube_url("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
assert is_valid is True
assert message == "URL is valid"
def test_validate_youtube_url_valid_without_www(self) -> None:
"""Test validation of YouTube URL without www."""
from youtube_mcp.validators import validate_youtube_url
is_valid, message = validate_youtube_url("https://youtube.com/watch?v=dQw4w9WgXcQ")
assert is_valid is True
assert message == "URL is valid"
def test_validate_youtube_url_valid_http(self) -> None:
"""Test validation of YouTube URL with http instead of https."""
from youtube_mcp.validators import validate_youtube_url
is_valid, message = validate_youtube_url("http://www.youtube.com/watch?v=dQw4w9WgXcQ")
assert is_valid is True
assert message == "URL is valid"
def test_validate_youtube_url_valid_playlist(self) -> None:
"""Test validation of YouTube watch_videos playlist URL."""
from youtube_mcp.validators import validate_youtube_url
is_valid, message = validate_youtube_url(
"https://www.youtube.com/watch_videos?video_ids=dQw4w9WgXcQ,9bZkp7q19f0"
)
assert is_valid is True
assert message == "URL is valid"
def test_validate_youtube_url_invalid_empty(self) -> None:
"""Test validation rejects empty URL."""
from youtube_mcp.validators import validate_youtube_url
is_valid, message = validate_youtube_url("")
assert is_valid is False
assert "must be a non-empty string" in message
def test_validate_youtube_url_invalid_wrong_domain(self) -> None:
"""Test validation rejects non-YouTube URL."""
from youtube_mcp.validators import validate_youtube_url
is_valid, message = validate_youtube_url("https://www.google.com/search?q=test")
assert is_valid is False
assert "valid YouTube" in message
def test_validate_youtube_url_invalid_type(self) -> None:
"""Test validation rejects non-string URL."""
from youtube_mcp.validators import validate_youtube_url
is_valid, message = validate_youtube_url(None) # type: ignore
assert is_valid is False
assert "must be a non-empty string" in message
def test_validate_api_key_valid(self) -> None:
"""Test validation of valid API key."""
from youtube_mcp.validators import validate_api_key
is_valid, message = validate_api_key("AIzaSyDummyTestKeyThatIsLongEnough")
assert is_valid is True
assert message == "API key is valid"
def test_validate_api_key_empty(self) -> None:
"""Test validation rejects empty API key."""
from youtube_mcp.validators import validate_api_key
is_valid, message = validate_api_key("")
assert is_valid is False
assert "non-empty string" in message
def test_validate_api_key_too_short(self) -> None:
"""Test validation rejects too short API key."""
from youtube_mcp.validators import validate_api_key
is_valid, message = validate_api_key("short")
assert is_valid is False
assert "invalid" in message.lower()
def test_validate_api_key_not_string(self) -> None:
"""Test validation rejects non-string API key."""
from youtube_mcp.validators import validate_api_key
is_valid, message = validate_api_key(12345) # type: ignore
assert is_valid is False
assert "non-empty string" in message
def test_validate_search_query_valid(self) -> None:
"""Test validation of valid search query."""
from youtube_mcp.validators import validate_search_query
is_valid, message = validate_search_query("Beatles - Let It Be")
assert is_valid is True
assert message == "Query is valid"
def test_validate_search_query_empty(self) -> None:
"""Test validation rejects empty query."""
from youtube_mcp.validators import validate_search_query
is_valid, message = validate_search_query("")
assert is_valid is False
assert "non-empty string" in message
def test_validate_search_query_too_long(self) -> None:
"""Test validation rejects query exceeding 300 characters."""
from youtube_mcp.validators import validate_search_query
long_query = "a" * 301
is_valid, message = validate_search_query(long_query)
assert is_valid is False
assert "300 characters" in message
def test_validate_search_query_max_length(self) -> None:
"""Test validation accepts query with exactly 300 characters."""
from youtube_mcp.validators import validate_search_query
query_300_chars = "a" * 300
is_valid, message = validate_search_query(query_300_chars)
assert is_valid is True
assert message == "Query is valid"
def test_validate_max_results_valid_minimum(self) -> None:
"""Test validation of minimum max_results value."""
from youtube_mcp.validators import validate_max_results
is_valid, message = validate_max_results(1)
assert is_valid is True
assert message == "max_results is valid"
def test_validate_max_results_valid_maximum(self) -> None:
"""Test validation of maximum max_results value."""
from youtube_mcp.validators import validate_max_results
is_valid, message = validate_max_results(50)
assert is_valid is True
assert message == "max_results is valid"
def test_validate_max_results_below_minimum(self) -> None:
"""Test validation rejects max_results below 1."""
from youtube_mcp.validators import validate_max_results
is_valid, message = validate_max_results(0)
assert is_valid is False
assert "between 1 and 50" in message
def test_validate_max_results_above_maximum(self) -> None:
"""Test validation rejects max_results above 50."""
from youtube_mcp.validators import validate_max_results
is_valid, message = validate_max_results(51)
assert is_valid is False
assert "between 1 and 50" in message
def test_validate_max_results_not_integer(self) -> None:
"""Test validation rejects non-integer max_results."""
from youtube_mcp.validators import validate_max_results
is_valid, message = validate_max_results(5.5) # type: ignore
assert is_valid is False
assert "must be an integer" in message
class TestBrowserControllerProcessManagement:
"""Test suite for BrowserController process management methods."""
@patch("subprocess.run")
def test_kill_process_windows_uses_taskkill(self, mock_run: Mock) -> None:
"""Test _kill_process_windows calls taskkill."""
BrowserController._kill_process_windows(12345)
mock_run.assert_called()
call_args = mock_run.call_args[0][0]
assert "taskkill" in call_args
assert "/PID" in call_args
assert "12345" in call_args
assert "/F" in call_args
assert "/T" in call_args
@patch("subprocess.run")
def test_kill_process_windows_fallback_to_powershell(self, mock_run: Mock) -> None:
"""Test _kill_process_windows falls back to PowerShell on taskkill failure."""
mock_run.side_effect = [FileNotFoundError(), None]
BrowserController._kill_process_windows(12345)
assert mock_run.call_count == 2
second_call_args = mock_run.call_args_list[1][0][0]
assert "powershell.exe" in second_call_args
@patch("os.kill")
def test_kill_process_unix_sends_sigterm(self, mock_kill: Mock) -> None:
"""Test _kill_process_unix sends SIGTERM first."""
BrowserController._kill_process_unix(12345)
mock_kill.assert_called()
first_call = mock_kill.call_args_list[0]
assert first_call[0][0] == 12345
assert first_call[0][1] == 15 # SIGTERM
@patch("os.kill")
def test_kill_process_unix_sends_sigkill_on_failure(self, mock_kill: Mock) -> None:
"""Test _kill_process_unix sends SIGKILL if SIGTERM fails."""
mock_kill.side_effect = OSError()
BrowserController._kill_process_unix(12345)
# Should attempt SIGTERM and then SIGKILL
assert mock_kill.call_count >= 1
@patch("subprocess.run")
def test_close_previous_browser_windows(self, mock_run: Mock) -> None:
"""Test _close_previous_browser kills process on Windows."""
with patch.object(BrowserController, "get_system", return_value="windows"):
controller = BrowserController()
controller.current_process_id = 12345
controller._close_previous_browser()
assert controller.current_process_id is None
mock_run.assert_called()
@patch("os.kill")
def test_close_previous_browser_unix(self, mock_kill: Mock) -> None:
"""Test _close_previous_browser kills process on Unix."""
with patch.object(BrowserController, "get_system", return_value="linux"):
controller = BrowserController()
controller.current_process_id = 12345
controller._close_previous_browser()
assert controller.current_process_id is None
mock_kill.assert_called()
def test_close_previous_browser_no_process(self) -> None:
"""Test _close_previous_browser handles None process ID gracefully."""
controller = BrowserController()
controller.current_process_id = None
# Should not raise any exception
controller._close_previous_browser()
assert controller.current_process_id is None
@patch("shutil.rmtree")
def test_close_previous_browser_cleans_temp_profile(self, mock_rmtree: Mock) -> None:
"""Test _close_previous_browser cleans up temp profile directory."""
with patch.object(BrowserController, "get_system", return_value="linux"):
with patch("os.kill"):
controller = BrowserController()
controller.current_process_id = 12345
controller._temp_profile_dir = "/tmp/test_profile"
controller._close_previous_browser()
mock_rmtree.assert_called_once()
assert controller._temp_profile_dir is None
class TestYouTubeClientAdvanced:
"""Advanced test suite for YouTubeClient with different regions and edge cases."""
@patch("youtube_mcp.youtube_client.build")
def test_search_song_default_region_code(self, mock_build: Mock) -> None:
"""Test that search_song uses ES as default region code."""
mock_youtube = MagicMock()
mock_build.return_value = mock_youtube
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_request = MagicMock()
mock_youtube.search().list.return_value = mock_request
mock_request.execute.return_value = {"items": []}
client = YouTubeClient(valid_api_key)
client.search_song("test")
call_kwargs = mock_youtube.search().list.call_args[1]
assert call_kwargs["regionCode"] == "ES"
@patch("youtube_mcp.youtube_client.build")
def test_search_song_multiple_results(self, mock_build: Mock) -> None:
"""Test that search_song handles multiple results correctly."""
mock_youtube = MagicMock()
mock_build.return_value = mock_youtube
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_request = MagicMock()
mock_youtube.search().list.return_value = mock_request
mock_request.execute.return_value = {
"items": [
{
"id": {"videoId": f"video{i}"},
"snippet": {
"title": f"Song {i}",
"channelTitle": f"Channel {i}",
},
}
for i in range(5)
]
}
client = YouTubeClient(valid_api_key)
results = client.search_song("test", max_results=5)
assert len(results) == 5
assert all("video_id" in r for r in results)
assert all("url" in r for r in results)
@patch("youtube_mcp.youtube_client.build")
def test_search_song_empty_results(self, mock_build: Mock) -> None:
"""Test that search_song handles empty results gracefully."""
mock_youtube = MagicMock()
mock_build.return_value = mock_youtube
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_request = MagicMock()
mock_youtube.search().list.return_value = mock_request
mock_request.execute.return_value = {"items": []}
client = YouTubeClient(valid_api_key)
results = client.search_song("nonexistent song xyz abc 123")
assert isinstance(results, list)
assert len(results) == 0
@patch("youtube_mcp.youtube_client.build")
def test_search_song_query_with_special_characters(self, mock_build: Mock) -> None:
"""Test that search_song accepts query with special characters."""
mock_youtube = MagicMock()
mock_build.return_value = mock_youtube
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_request = MagicMock()
mock_youtube.search().list.return_value = mock_request
mock_request.execute.return_value = {"items": []}
client = YouTubeClient(valid_api_key)
client.search_song("The Beatles - Help! (Remastered)")
mock_youtube.search().list.assert_called_once()
class TestServerIntegration:
"""Integration tests for server with multiple operations."""
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_handle_search_youtube_with_max_results(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test handle_search_youtube with custom max_results."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_client_instance = MagicMock()
mock_youtube.return_value = mock_client_instance
mock_client_instance.search_song.return_value = [
{
"title": f"Song {i}",
"channel": f"Channel {i}",
"video_id": f"video{i}",
"url": f"https://www.youtube.com/watch?v=video{i}",
}
for i in range(3)
]
server = YouTubeMCPServer(valid_api_key)
result = server.handle_search_youtube("test", max_results=3)
assert result["success"] is True
assert result["count"] == 3
mock_client_instance.search_song.assert_called_once_with("test", 3)
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_handle_play_youtube_calls_close_previous(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that handle_play_youtube closes previous browser."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
mock_browser_instance = MagicMock()
mock_browser.return_value = mock_browser_instance
mock_browser_instance.open_youtube_url.return_value = {
"success": True,
"message": "Now playing",
"system": "linux",
"url": "https://www.youtube.com/watch?v=test123",
}
server = YouTubeMCPServer(valid_api_key)
server.handle_play_youtube("https://www.youtube.com/watch?v=test123")
mock_browser_instance._close_previous_browser.assert_called_once()
@patch("youtube_mcp.server.YouTubeClient")
@patch("youtube_mcp.server.BrowserController")
def test_server_tools_contain_descriptions(
self, mock_browser: Mock, mock_youtube: Mock
) -> None:
"""Test that all tools have descriptions."""
valid_api_key = "AIzaSyDummyTestKeyThatIsLongEnough"
server = YouTubeMCPServer(valid_api_key)
tools = server.get_tools()
for tool in tools:
assert "description" in tool
assert len(tool["description"]) > 0
assert "inputSchema" in tool
assert "properties" in tool["inputSchema"]
assert "required" in tool["inputSchema"]