Radarr and Sonarr MCP Server

  • tests
"""Tests for the MCP server implementation.""" import unittest import json import os import sys import tempfile from unittest.mock import patch, MagicMock # Add parent directory to path to import the package sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from radarr_sonarr_mcp.config import Config, NasConfig, RadarrConfig, SonarrConfig, ServerConfig from radarr_sonarr_mcp.server import RadarrSonarrMCPServer, create_server from radarr_sonarr_mcp.services.radarr_service import Movie from radarr_sonarr_mcp.services.sonarr_service import Series, Statistics class TestRadarrSonarrMCPServer(unittest.TestCase): """Test suite for the RadarrSonarrMCPServer class.""" def setUp(self): """Set up test environment.""" # Create a temporary config file self.temp_file = tempfile.NamedTemporaryFile(delete=False) config_data = { "nasConfig": { "ip": "127.0.0.1", "port": "7878" }, "radarrConfig": { "apiKey": "test_radarr_api_key", "basePath": "/api/v3", "port": "7878" }, "sonarrConfig": { "apiKey": "test_sonarr_api_key", "basePath": "/api/v3", "port": "8989" }, "server": { "port": 5000 } } with open(self.temp_file.name, 'w') as f: json.dump(config_data, f) # Create sample movie and series data self.sample_movies = [ Movie( id=1, title="Test Movie 1", year=2022, overview="A test movie", has_file=True, status="downloaded", genres=["Action", "Comedy"], tags=[1, 2], data={ "id": 1, "title": "Test Movie 1", "year": 2022, "overview": "A test movie", "hasFile": True, "status": "downloaded", "genres": ["Action", "Comedy"], "tags": [1, 2], "credits": { "cast": [ {"name": "Actor One", "character": "Character One"}, {"name": "Actress One", "character": "Character Two"} ] } } ), Movie( id=2, title="Test Movie 2", year=2023, overview="Another test movie", has_file=False, status="wanted", genres=["Drama", "Thriller"], tags=[2], data={ "id": 2, "title": "Test Movie 2", "year": 2023, "overview": "Another test movie", "hasFile": False, "status": "wanted", "genres": ["Drama", "Thriller"], "tags": [2], "credits": { "cast": [ {"name": "Actor Two", "character": "Character Three"} ] } } ) ] self.sample_series = [ Series( id=1, title="Test Series 1", year=2022, overview="A test series", status="continuing", network="Test Network", tags=[1], genres=["Comedy"], statistics=Statistics.from_dict({ "episodeFileCount": 10, "episodeCount": 10, "totalEpisodeCount": 20, "sizeOnDisk": 10000 }), data={ "id": 1, "title": "Test Series 1", "year": 2022, "overview": "A test series", "status": "continuing", "network": "Test Network", "tags": [1], "genres": ["Comedy"], "statistics": { "episodeFileCount": 10, "episodeCount": 10, "totalEpisodeCount": 20, "sizeOnDisk": 10000 }, "credits": { "cast": [ {"name": "Actor Three", "character": "Character Four"} ] } } ) ] def tearDown(self): """Clean up after tests.""" self.temp_file.close() os.unlink(self.temp_file.name) @patch('radarr_sonarr_mcp.server.FastMCP') def test_server_initialization(self, mock_fastmcp): """Test server initialization with config file.""" server = create_server(self.temp_file.name) self.assertEqual(server.config.radarr_config.api_key, "test_radarr_api_key") self.assertEqual(server.config.sonarr_config.api_key, "test_sonarr_api_key") self.assertEqual(server.config.server_config.port, 5000) # Check that FastMCP was initialized correctly mock_fastmcp.assert_called_once() self.assertEqual(mock_fastmcp.call_args[1]['name'], "radarr-sonarr-mcp-server") @patch('radarr_sonarr_mcp.server.RadarrService') @patch('radarr_sonarr_mcp.server.SonarrService') @patch('radarr_sonarr_mcp.server.FastMCP') def test_get_available_movies(self, mock_fastmcp, mock_sonarr_service, mock_radarr_service): """Test the get_available_movies tool.""" # Setup mocks mock_radarr_instance = mock_radarr_service.return_value mock_radarr_instance.get_all_movies.return_value = self.sample_movies mock_radarr_instance.is_movie_watched.return_value = True mock_radarr_instance.is_movie_in_watchlist.return_value = False mock_server = mock_fastmcp.return_value # Create server and register tools server = create_server(self.temp_file.name) # Extract the registered tool function tool_decorator = mock_server.tool.return_value get_movies_func = None for call in tool_decorator.call_args_list: # The decorated function is passed to the decorator if call.args and call.args[0].__name__ == 'get_available_movies': get_movies_func = call.args[0] break self.assertIsNotNone(get_movies_func, "get_available_movies tool not registered") # Test the tool function result = get_movies_func(year=2022) result_data = json.loads(result) # Check results self.assertEqual(result_data['count'], 1) self.assertEqual(result_data['movies'][0]['title'], "Test Movie 1") self.assertEqual(result_data['movies'][0]['year'], 2022) @patch('radarr_sonarr_mcp.server.SonarrService') @patch('radarr_sonarr_mcp.server.RadarrService') @patch('radarr_sonarr_mcp.server.FastMCP') def test_get_available_series(self, mock_fastmcp, mock_radarr_service, mock_sonarr_service): """Test the get_available_series tool.""" # Setup mocks mock_sonarr_instance = mock_sonarr_service.return_value mock_sonarr_instance.get_all_series.return_value = self.sample_series mock_sonarr_instance.is_series_watched.return_value = True mock_sonarr_instance.is_series_in_watchlist.return_value = False mock_server = mock_fastmcp.return_value # Create server and register tools server = create_server(self.temp_file.name) # Extract the registered tool function tool_decorator = mock_server.tool.return_value get_series_func = None for call in tool_decorator.call_args_list: # The decorated function is passed to the decorator if call.args and call.args[0].__name__ == 'get_available_series': get_series_func = call.args[0] break self.assertIsNotNone(get_series_func, "get_available_series tool not registered") # Test the tool function result = get_series_func() result_data = json.loads(result) # Check results self.assertEqual(result_data['count'], 1) self.assertEqual(result_data['series'][0]['title'], "Test Series 1") self.assertEqual(result_data['series'][0]['year'], 2022) @patch('radarr_sonarr_mcp.server.RadarrService') @patch('radarr_sonarr_mcp.server.SonarrService') @patch('radarr_sonarr_mcp.server.FastMCP') def test_server_resources(self, mock_fastmcp, mock_sonarr_service, mock_radarr_service): """Test registered resources.""" # Setup mocks mock_radarr_instance = mock_radarr_service.return_value mock_radarr_instance.get_all_movies.return_value = self.sample_movies mock_sonarr_instance = mock_sonarr_service.return_value mock_sonarr_instance.get_all_series.return_value = self.sample_series mock_server = mock_fastmcp.return_value # Create server and register resources server = create_server(self.temp_file.name) # Mock get_resource_handler to return MagicMock objects mock_server_instance = mock_fastmcp.return_value mock_server_instance.get_resource_handler.side_effect = lambda path: MagicMock(return_value={ '/movies': {"count": 2, "movies": self.sample_movies}, '/series': {"count": 1, "series": self.sample_series} }.get(path)) # Test movies resource movies_resource = server.server.get_resource_handler('/movies') self.assertIsNotNone(movies_resource, "Movies resource not registered") result_movies = movies_resource() self.assertEqual(result_movies['count'], 2) self.assertEqual(len(result_movies['movies']), 2) # Test series resource series_resource = server.server.get_resource_handler('/series') self.assertIsNotNone(series_resource, "Series resource not registered") result_series = series_resource() self.assertEqual(result_series['count'], 1) self.assertEqual(len(result_series['series']), 1) if __name__ == '__main__': unittest.main()