import inspect import ssl from unittest.mock import AsyncMock, MagicMock, patch import pytest from streamrip.client.client import Client from streamrip.client.qobuz import QobuzSpoofer from streamrip.rip.cli import latest_streamrip_version, rip from streamrip.utils.ssl_utils import ( create_ssl_context, get_aiohttp_connector_kwargs, print_ssl_error_help, ) @pytest.fixture def mock_client_session(): """Fixture that provides a mocked aiohttp.ClientSession.""" with patch("aiohttp.ClientSession") as mock_session: mock_session.return_value = AsyncMock() yield mock_session @pytest.fixture def mock_tcp_connector(): """Fixture that provides a mocked aiohttp.TCPConnector.""" with patch("aiohttp.TCPConnector") as mock_connector: mock_connector.return_value = MagicMock() yield mock_connector @pytest.fixture def mock_ssl_context(): """Fixture that provides a mocked SSL context.""" with patch("ssl.create_default_context") as mock_ctx: mock_ctx.return_value = MagicMock() yield mock_ctx @pytest.fixture def mock_certifi(): """Fixture that provides a mocked certifi module.""" with patch("streamrip.utils.ssl_utils.HAS_CERTIFI", True): with patch("streamrip.utils.ssl_utils.certifi") as mock_cert: mock_cert.where.return_value = "/path/to/mock/cacert.pem" yield mock_cert def test_create_ssl_context_with_verification(mock_ssl_context): """Test that create_ssl_context creates a proper SSL context with verification enabled.""" # Call the function with verification enabled ctx = create_ssl_context(verify=True) # Verify create_default_context was called mock_ssl_context.assert_called_once() # Function should return the mocked context assert ctx == mock_ssl_context.return_value def test_create_ssl_context_without_verification(mock_ssl_context): """Test that create_ssl_context disables verification when requested.""" # Call the function with verification disabled ctx = create_ssl_context(verify=False) # Verify create_default_context was called mock_ssl_context.assert_called_once() # Check that verification was disabled on the context assert ctx.check_hostname is False assert ctx.verify_mode == ssl.CERT_NONE def test_create_ssl_context_with_certifi(mock_ssl_context, mock_certifi): """Test that create_ssl_context uses certifi when available.""" # Call the function create_ssl_context(verify=True) # Verify certifi.where was called mock_certifi.where.assert_called_once() # Verify create_default_context was called with the certifi path mock_ssl_context.assert_called_once_with(cafile=mock_certifi.where.return_value) def test_get_aiohttp_connector_kwargs_with_verification(mock_ssl_context, mock_certifi): """Test get_aiohttp_connector_kwargs with verification enabled with certifi.""" # Mock the create_ssl_context function to control its return value with patch("streamrip.utils.ssl_utils.create_ssl_context") as mock_create_ctx: mock_ssl_ctx = MagicMock() mock_create_ctx.return_value = mock_ssl_ctx # Call the function with verification enabled kwargs = get_aiohttp_connector_kwargs(verify_ssl=True) # When certifi is available, it should return kwargs with ssl context assert "ssl" in kwargs assert kwargs["ssl"] == mock_ssl_ctx def test_get_aiohttp_connector_kwargs_without_verification(): """Test get_aiohttp_connector_kwargs with verification disabled.""" # Call the function with verification disabled kwargs = get_aiohttp_connector_kwargs(verify_ssl=False) # It should return kwargs with verify_ssl=False assert kwargs == {"verify_ssl": False} def test_client_get_session_supports_verify_ssl(): """Test that Client.get_session supports verify_ssl parameter.""" # Check if the get_session method accepts the verify_ssl parameter signature = inspect.signature(Client.get_session) # Check for verify_ssl parameter has_verify_ssl = "verify_ssl" in signature.parameters # Skip rather than fail if option isn't implemented yet if not has_verify_ssl: pytest.skip("verify_ssl parameter not implemented in Client.get_session yet") @pytest.mark.asyncio async def test_client_get_session_creates_connector(): """Test that Client.get_session creates a session with correct parameters.""" # Check if the get_session method accepts the verify_ssl parameter signature = inspect.signature(Client.get_session) # Skip if verify_ssl is not in parameters if "verify_ssl" not in signature.parameters: pytest.skip("verify_ssl parameter not implemented in Client.get_session yet") # Patch the get_aiohttp_connector_kwargs function and the client session with ( patch( "streamrip.client.client.get_aiohttp_connector_kwargs" ) as mock_get_kwargs, patch("aiohttp.ClientSession") as mock_client_session, patch("aiohttp.TCPConnector") as mock_connector, ): mock_get_kwargs.return_value = {"verify_ssl": False} mock_connector.return_value = MagicMock() mock_client_session.return_value = AsyncMock() # Test with SSL verification disabled await Client.get_session(verify_ssl=False) # Verify get_aiohttp_connector_kwargs was called with verify_ssl=False mock_get_kwargs.assert_called_once_with(verify_ssl=False) def test_latest_streamrip_version_supports_verify_ssl(): """Test that latest_streamrip_version supports verify_ssl parameter.""" # Check if the function accepts the verify_ssl parameter signature = inspect.signature(latest_streamrip_version) # Check for verify_ssl parameter has_verify_ssl = "verify_ssl" in signature.parameters # Skip rather than fail if option isn't implemented yet if not has_verify_ssl: pytest.skip( "verify_ssl parameter not implemented in latest_streamrip_version yet" ) @pytest.mark.asyncio async def test_latest_streamrip_version_creates_session(): """Test that latest_streamrip_version creates a session with verify_ssl parameter.""" # Check if the function accepts the verify_ssl parameter signature = inspect.signature(latest_streamrip_version) # Skip if verify_ssl is not in parameters if "verify_ssl" not in signature.parameters: pytest.skip( "verify_ssl parameter not implemented in latest_streamrip_version yet" ) # Patch the get_aiohttp_connector_kwargs function and related modules with ( patch("streamrip.rip.cli.get_aiohttp_connector_kwargs") as mock_get_kwargs, patch("aiohttp.ClientSession") as mock_client_session, patch("aiohttp.TCPConnector") as mock_connector, ): mock_get_kwargs.return_value = {"verify_ssl": False} mock_connector.return_value = MagicMock() # Setup mock responses for API calls mock_session_instance = AsyncMock() mock_client_session.return_value = mock_session_instance mock_context_manager = AsyncMock() mock_session_instance.get.return_value = mock_context_manager mock_context_manager.__aenter__.return_value.json.return_value = { "info": {"version": "1.0.0"} } # Make sure the test doesn't actually wait with patch("streamrip.rip.cli.__version__", "1.0.0"): # Run with SSL verification parameter try: await latest_streamrip_version(verify_ssl=False) except Exception: # We just need to ensure it doesn't raise TypeError for the verify_ssl parameter pass # Verify get_aiohttp_connector_kwargs was called with verify_ssl=False mock_get_kwargs.assert_called_once_with(verify_ssl=False) @pytest.mark.asyncio async def test_qobuz_spoofer_initialization(mock_client_session): """Test that QobuzSpoofer initialization works with available parameters.""" # Check if QobuzSpoofer accepts verify_ssl parameter signature = inspect.signature(QobuzSpoofer.__init__) has_verify_ssl = "verify_ssl" in signature.parameters # Create instance based on available parameters if has_verify_ssl: # Patch the get_aiohttp_connector_kwargs function for the __aenter__ method with patch( "streamrip.utils.ssl_utils.get_aiohttp_connector_kwargs" ) as mock_get_kwargs: mock_get_kwargs.return_value = {"verify_ssl": True} spoofer = QobuzSpoofer(verify_ssl=True) assert spoofer is not None # Test __aenter__ and __aexit__ with patch.object(spoofer, "session", None): await spoofer.__aenter__() # Verify get_aiohttp_connector_kwargs was called mock_get_kwargs.assert_called_once_with(verify_ssl=True) # Verify ClientSession was called assert mock_client_session.called await spoofer.__aexit__(None, None, None) else: spoofer = QobuzSpoofer() assert spoofer is not None with patch.object(spoofer, "session", None): await spoofer.__aenter__() assert mock_client_session.called await spoofer.__aexit__(None, None, None) @pytest.mark.asyncio async def test_lastfm_playlist_session_creation(mock_client_session): """Test that PendingLastfmPlaylist creates a ClientSession.""" from streamrip.media.playlist import PendingLastfmPlaylist # Mock objects needed for playlist mock_client = MagicMock() mock_fallback_client = MagicMock() mock_config = MagicMock() mock_db = MagicMock() # Create instance pending_playlist = PendingLastfmPlaylist( "https://www.last.fm/test", mock_client, mock_fallback_client, mock_config, mock_db, ) # Check if our code expects verify_ssl in config try: mock_config.session.downloads.verify_ssl = False with patch( "streamrip.utils.ssl_utils.get_aiohttp_connector_kwargs" ) as mock_get_kwargs: mock_get_kwargs.return_value = {"verify_ssl": False} # Try to parse the playlist with pytest.raises(Exception): await pending_playlist._parse_lastfm_playlist() except (AttributeError, TypeError): pytest.skip( "verify_ssl not used in PendingLastfmPlaylist._parse_lastfm_playlist yet" ) @pytest.mark.asyncio async def test_client_uses_config_settings(): """Test that clients use SSL verification settings from config.""" from streamrip.client.tidal import TidalClient # Mock the config with patch("streamrip.config.Config") as mock_config: mock_config = MagicMock() mock_config.return_value = mock_config # Set verify_ssl in config mock_config.session.downloads.verify_ssl = False # Create client try: client = TidalClient(mock_config) # Mock the session creation method with patch.object(client, "get_session", AsyncMock()) as mock_get_session: await client.login() # Check that get_session was called with verify_ssl=False mock_get_session.assert_called_once() try: # Try to access the call args to check for verify_ssl call_kwargs = mock_get_session.call_args.kwargs assert "verify_ssl" in call_kwargs assert call_kwargs["verify_ssl"] is False except (AttributeError, AssertionError): pytest.skip("verify_ssl not used in TidalClient.login yet") except Exception as e: pytest.skip(f"Could not test TidalClient: {e}") def test_cli_option_registered(): """Test that the --no-ssl-verify CLI option is registered.""" # Check if the option exists in the command parameters has_no_ssl_verify = False for param in rip.params: if getattr(param, "name", "") == "no_ssl_verify": has_no_ssl_verify = True break assert has_no_ssl_verify, "CLI command should accept --no-ssl-verify option" def test_error_handling_with_ssl_errors(): """Test the error handling output with SSL errors.""" with patch("sys.stdout"), patch("sys.exit") as mock_exit: # Call the function print_ssl_error_help() # Check exit code mock_exit.assert_called_once_with(1)