import signal import socket from unittest.mock import patch from pytest import raises from paramiko import ProxyCommand, ProxyCommandFailure class TestProxyCommand: @patch("paramiko.proxy.subprocess") def test_init_takes_command_string(self, subprocess): ProxyCommand(command_line="do a thing") subprocess.Popen.assert_called_once_with( ["do", "a", "thing"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, ) @patch("paramiko.proxy.subprocess.Popen") def test_send_writes_to_process_stdin_returning_length(self, Popen): proxy = ProxyCommand("hi") written = proxy.send(b"data") Popen.return_value.stdin.write.assert_called_once_with(b"data") assert written == len(b"data") @patch("paramiko.proxy.subprocess.Popen") def test_send_raises_ProxyCommandFailure_on_error(self, Popen): Popen.return_value.stdin.write.side_effect = IOError(0, "whoops") with raises(ProxyCommandFailure) as info: ProxyCommand("hi").send("data") assert info.value.command == "hi" assert info.value.error == "whoops" @patch("paramiko.proxy.subprocess.Popen") @patch("paramiko.proxy.os.read") @patch("paramiko.proxy.select") def test_recv_reads_from_process_stdout_returning_bytes( self, select, os_read, Popen ): stdout = Popen.return_value.stdout select.return_value = [stdout], None, None fileno = stdout.fileno.return_value # Force os.read to return smaller-than-requested chunks os_read.side_effect = [b"was", b"t", b"e", b"of ti", b"me"] proxy = ProxyCommand("hi") # Ask for 5 bytes (ie b"waste") data = proxy.recv(5) # Ensure we got "waste" stitched together assert data == b"waste" # Ensure the calls happened in the sizes expected (starting with the # initial "I want all 5 bytes", followed by "I want whatever I believe # should be left after what I've already read", until done) assert [x[0] for x in os_read.call_args_list] == [ (fileno, 5), # initial (fileno, 2), # I got 3, want 2 more (fileno, 1), # I've now got 4, want 1 more ] @patch("paramiko.proxy.subprocess.Popen") @patch("paramiko.proxy.os.read") @patch("paramiko.proxy.select") def test_recv_returns_buffer_on_timeout_if_any_read( self, select, os_read, Popen ): stdout = Popen.return_value.stdout select.return_value = [stdout], None, None fileno = stdout.fileno.return_value os_read.side_effect = [b"was", socket.timeout] proxy = ProxyCommand("hi") data = proxy.recv(5) assert data == b"was" # not b"waste" assert os_read.call_args[0] == (fileno, 2) @patch("paramiko.proxy.subprocess.Popen") @patch("paramiko.proxy.os.read") @patch("paramiko.proxy.select") def test_recv_raises_timeout_if_nothing_read(self, select, os_read, Popen): stdout = Popen.return_value.stdout select.return_value = [stdout], None, None fileno = stdout.fileno.return_value os_read.side_effect = socket.timeout proxy = ProxyCommand("hi") with raises(socket.timeout): proxy.recv(5) assert os_read.call_args[0] == (fileno, 5) @patch("paramiko.proxy.subprocess.Popen") @patch("paramiko.proxy.os.read") @patch("paramiko.proxy.select") def test_recv_raises_ProxyCommandFailure_on_non_timeout_error( self, select, os_read, Popen ): select.return_value = [Popen.return_value.stdout], None, None os_read.side_effect = IOError(0, "whoops") with raises(ProxyCommandFailure) as info: ProxyCommand("hi").recv(5) assert info.value.command == "hi" assert info.value.error == "whoops" @patch("paramiko.proxy.subprocess.Popen") @patch("paramiko.proxy.os.kill") def test_close_kills_subprocess(self, os_kill, Popen): proxy = ProxyCommand("hi") proxy.close() os_kill.assert_called_once_with(Popen.return_value.pid, signal.SIGTERM) @patch("paramiko.proxy.subprocess.Popen") def test_closed_exposes_whether_subprocess_has_exited(self, Popen): proxy = ProxyCommand("hi") Popen.return_value.returncode = None assert proxy.closed is False assert proxy._closed is False Popen.return_value.returncode = 0 assert proxy.closed is True assert proxy._closed is True @patch("paramiko.proxy.time.time") @patch("paramiko.proxy.subprocess.Popen") @patch("paramiko.proxy.os.read") @patch("paramiko.proxy.select") def test_timeout_affects_whether_timeout_is_raised( self, select, os_read, Popen, time ): stdout = Popen.return_value.stdout select.return_value = [stdout], None, None # Base case: None timeout means no timing out os_read.return_value = b"meh" proxy = ProxyCommand("hello") assert proxy.timeout is None # Implicit 'no raise' check assert proxy.recv(3) == b"meh" # Use settimeout to set timeout, and it is honored time.side_effect = [0, 10] # elapsed > 7 proxy = ProxyCommand("ohnoz") proxy.settimeout(7) assert proxy.timeout == 7 with raises(socket.timeout): proxy.recv(3) @patch("paramiko.proxy.subprocess", new=None) @patch("paramiko.proxy.subprocess_import_error", new=ImportError("meh")) def test_raises_subprocess_ImportErrors_at_runtime(self): # Not an ideal test, but I don't know of a non-bad way to fake out # module-time ImportErrors. So we mock the symptoms. Meh! with raises(ImportError) as info: ProxyCommand("hi!!!") assert str(info.value) == "meh"