diff --git a/.gitignore b/.gitignore
index 7d6a3ec9..61a93d79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ __pycache__
/winrm/tests/config.json
.pytest_cache
venv
+.tox
diff --git a/requirements-test.txt b/requirements-test.txt
index aa82eab0..ff85389b 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -4,3 +4,4 @@ pytest==4.4.2
pytest-cov==2.7.1
pytest-flake8==1.0.4
mock==3.0.5
+requests_mock==1.5.2
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..c1a6340d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+xmltodict
+requests>=2.9.1
+requests_ntlm>=0.3.0
+six>=1.7.0
\ No newline at end of file
diff --git a/requirements/extras/requirements-credssp.txt b/requirements/extras/requirements-credssp.txt
new file mode 100644
index 00000000..f6b73ec7
--- /dev/null
+++ b/requirements/extras/requirements-credssp.txt
@@ -0,0 +1 @@
+requests-credssp>=1.0.0
diff --git a/requirements/extras/requirements-kerberos.txt b/requirements/extras/requirements-kerberos.txt
new file mode 100644
index 00000000..ede9cd09
--- /dev/null
+++ b/requirements/extras/requirements-kerberos.txt
@@ -0,0 +1,2 @@
+winkerberos>=0.5.0 ; sys_platform=="win32"
+pykerberos>=1.2.1,<2.0.0 ; sys_platform!="win32"
\ No newline at end of file
diff --git a/requirements/requirements-testmin.txt b/requirements/requirements-testmin.txt
new file mode 100644
index 00000000..c8d5f9e0
--- /dev/null
+++ b/requirements/requirements-testmin.txt
@@ -0,0 +1,13 @@
+xmltodict==0.3.0
+requests==2.9.1
+requests_ntlm==0.3.0
+six==1.7.0
+
+# credssp
+requests-credssp==1.0.0
+
+# kerberos
+winkerberos==0.5.0 ; sys_platform=="win32"
+pykerberos==1.2.1 ; sys_platform!="win32"
+
+-r ../requirements-test.txt
\ No newline at end of file
diff --git a/setup.py b/setup.py
index b71ec2f2..05b59d05 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,5 @@
+import os
+
from setuptools import setup
__version__ = '0.3.1.dev0'
@@ -11,6 +13,29 @@
except ImportError:
long_description = ''
+
+def install_deps():
+ default = open('requirements.txt', 'r').readlines()
+ pkg_list = []
+ for resource in default:
+ pkg_list.append(resource.strip())
+ return pkg_list
+
+
+def install_extras():
+ extras = dict()
+ for filename in os.listdir('requirements/extras'):
+ extra_key = filename.replace('requirements-', '').replace('.txt', '')
+ extras[extra_key] = list()
+ with open("requirements/extras/" + filename, 'r') as req_file:
+ for pkg_name in req_file.readlines():
+ extras[extra_key].append(pkg_name)
+ return extras
+
+
+req_deps_list = install_deps()
+extras_deps = install_extras()
+
setup(
name=project_name,
version=__version__,
@@ -23,12 +48,8 @@
license='MIT license',
packages=('winrm', 'winrm.tests', 'winrm.vendor.requests_kerberos'),
package_data={'winrm.tests': ['*.ps1']},
- install_requires=['xmltodict', 'requests>=2.9.1', 'requests_ntlm>=0.3.0', 'six'],
- extras_require={
- 'credssp': ['requests-credssp>=1.0.0'],
- 'kerberos:sys_platform=="win32"': ['winkerberos>=0.5.0'],
- 'kerberos:sys_platform!="win32"': ['pykerberos>=1.2.1,<2.0.0']
- },
+ install_requires=req_deps_list,
+ extras_require=extras_deps,
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..2bc61218
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,67 @@
+[tox]
+envlist = py27,py35,py36,py37,py38,py27min,py37min,py27base,py37base
+skip_missing_interpreters = True
+skipsdist = True
+sitepackages = False
+
+[testenv]
+ignore_errors = False
+deps =
+ -r{toxinidir}/requirements-test.txt
+ -r{toxinidir}/requirements/extras/requirements-credssp.txt
+ -r{toxinidir}/requirements/extras/requirements-kerberos.txt
+ -r{toxinidir}/requirements.txt
+setenv =
+ PYWINRM_TEST_CREDSSP=1
+ PYWINRM_TEST_KERBEROS=1
+commands =
+ pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/
+whitelist_externals = bash
+
+# Test the base install, without extras.
+[testenv:py27base]
+ignore_errors = False
+deps =
+ -r{toxinidir}/requirements-test.txt
+ -r{toxinidir}/requirements.txt
+setenv =
+ PYWINRM_TEST_CREDSSP=0
+ PYWINRM_TEST_KERBEROS=0
+commands =
+ pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/
+whitelist_externals = bash
+
+[testenv:py37base]
+ignore_errors = False
+deps =
+ -r{toxinidir}/requirements-test.txt
+ -r{toxinidir}/requirements.txt
+setenv =
+ PYWINRM_TEST_CREDSSP=0
+ PYWINRM_TEST_KERBEROS=0
+commands =
+ pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/
+whitelist_externals = bash
+
+# Test the minimum requirements for the packages.
+[testenv:py27min]
+ignore_errors = False
+deps =
+ -r{toxinidir}/requirements/requirements-testmin.txt
+setenv =
+ PYWINRM_TEST_CREDSSP=1
+ PYWINRM_TEST_KERBEROS=1
+commands =
+ pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/
+whitelist_externals = bash
+
+[testenv:py37min]
+ignore_errors = False
+deps =
+ -r{toxinidir}/requirements/requirements-testmin.txt
+setenv =
+ PYWINRM_TEST_CREDSSP=1
+ PYWINRM_TEST_KERBEROS=1
+commands =
+ pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/
+whitelist_externals = bash
diff --git a/winrm/tests/base.py b/winrm/tests/base.py
new file mode 100644
index 00000000..0eafc277
--- /dev/null
+++ b/winrm/tests/base.py
@@ -0,0 +1,45 @@
+import os
+import unittest
+import requests_mock
+from winrm import transport
+
+if os.environ.get('PYWINRM_TEST_CREDSSP') == '1':
+ EXPECT_CREDSSP = True
+elif os.environ.get('PYWINRM_TEST_CREDSSP') == '0':
+ EXPECT_CREDSSP = False
+else:
+ EXPECT_CREDSSP = transport.HAVE_CREDSSP
+
+if os.environ.get('PYWINRM_TEST_KERBEROS') == '1':
+ EXPECT_KERBEROS = True
+elif os.environ.get('PYWINRM_TEST_KERBEROS') == '0':
+ EXPECT_KERBEROS = False
+else:
+ EXPECT_KERBEROS = transport.HAVE_KERBEROS
+
+
+class BaseTest(unittest.TestCase):
+ maxDiff = 2048
+ _old_env = None
+
+ def setUp(self):
+ super(BaseTest, self).setUp()
+ self.mocked_request = requests_mock.Mocker()
+ self.mocked_request.start()
+ self._old_env = {}
+ os.environ.pop('REQUESTS_CA_BUNDLE', None)
+ os.environ.pop('TRAVIS_APT_PROXY', None)
+ os.environ.pop('CURL_CA_BUNDLE', None)
+ os.environ.pop('HTTPS_PROXY', None)
+ os.environ.pop('HTTP_PROXY', None)
+ os.environ.pop('NO_PROXY', None)
+
+ def tearDown(self):
+ super(BaseTest, self).tearDown()
+ os.environ.pop('REQUESTS_CA_BUNDLE', None)
+ os.environ.pop('TRAVIS_APT_PROXY', None)
+ os.environ.pop('CURL_CA_BUNDLE', None)
+ os.environ.pop('HTTPS_PROXY', None)
+ os.environ.pop('HTTP_PROXY', None)
+ os.environ.pop('NO_PROXY', None)
+ self.mocked_request.stop()
diff --git a/winrm/tests/test_protocol.py b/winrm/tests/test_protocol.py
index bcea47ea..98084ce6 100644
--- a/winrm/tests/test_protocol.py
+++ b/winrm/tests/test_protocol.py
@@ -1,8 +1,22 @@
import pytest
+import copy
+import xmltodict
+from winrm.tests import base as base_test
+from winrm.tests.winrm_responses import shells as shell_responses
from winrm.protocol import Protocol
+def convert_to_dict(ordered_dict):
+ new_dict = dict()
+ for name, value in ordered_dict.items():
+ if isinstance(value, dict):
+ new_dict[name] = convert_to_dict(value)
+ else:
+ new_dict[name] = value
+ return new_dict
+
+
def test_open_shell_and_close_shell(protocol_fake):
shell_id = protocol_fake.open_shell()
assert shell_id == '11111111-1111-1111-1111-111111111113'
@@ -71,3 +85,23 @@ def test_fail_set_operation_timeout_as_sec():
operation_timeout_sec='29a')
assert str(exc.value) == "failed to parse operation_timeout_sec as int: " \
"invalid literal for int() with base 10: '29a'"
+
+
+class TestTransportShells(base_test.BaseTest):
+
+ def test_open_shell(self):
+ self.mocked_request.post('https://example.com', text=shell_responses.OPEN_SHELL_RESPONSE)
+ server_conn = Protocol(endpoint="https://example.com",
+ username='test',
+ password='test',
+ )
+ response = server_conn.open_shell()
+ self.assertEqual('5207F2DF-E6CA-4D10-8C7F-5380F01D6FDE', response)
+
+ # MessageID will be dynamic, no need to compare
+ expected_shell_request = xmltodict.parse(copy.deepcopy(shell_responses.OPEN_SHELL_REQUEST))
+ actual_shell_request = xmltodict.parse(copy.deepcopy(self.mocked_request.request_history[0].body))
+ del expected_shell_request['env:Envelope']['env:Header']['a:MessageID']
+ del actual_shell_request['env:Envelope']['env:Header']['a:MessageID']
+
+ self.assertEqual(convert_to_dict(expected_shell_request), convert_to_dict(actual_shell_request))
diff --git a/winrm/tests/test_transport.py b/winrm/tests/test_transport.py
index cc451f3f..6ab15f55 100644
--- a/winrm/tests/test_transport.py
+++ b/winrm/tests/test_transport.py
@@ -2,36 +2,23 @@
import os
import mock
import unittest
+import requests
+from . import base as base_test
+from distutils.version import StrictVersion
from winrm import transport
from winrm.exceptions import WinRMError, InvalidCredentialsError
+REQUEST_VERSION = requests.__version__.split('.')
-class TestTransport(unittest.TestCase):
- maxDiff = 2048
- _old_env = None
+
+class TestTransport(base_test.BaseTest):
def setUp(self):
super(TestTransport, self).setUp()
- self._old_env = {}
- os.environ.pop('REQUESTS_CA_BUNDLE', None)
- os.environ.pop('TRAVIS_APT_PROXY', None)
- os.environ.pop('CURL_CA_BUNDLE', None)
- os.environ.pop('HTTPS_PROXY', None)
- os.environ.pop('HTTP_PROXY', None)
- os.environ.pop('NO_PROXY', None)
transport.DISPLAYED_PROXY_WARNING = False
transport.DISPLAYED_CA_TRUST_WARNING = False
- def tearDown(self):
- super(TestTransport, self).tearDown()
- os.environ.pop('REQUESTS_CA_BUNDLE', None)
- os.environ.pop('TRAVIS_APT_PROXY', None)
- os.environ.pop('CURL_CA_BUNDLE', None)
- os.environ.pop('HTTPS_PROXY', None)
- os.environ.pop('HTTP_PROXY', None)
- os.environ.pop('NO_PROXY', None)
-
def test_build_session_cert_validate_default(self):
t_default = transport.Transport(endpoint="https://example.com",
username='test',
@@ -144,6 +131,24 @@ def test_build_session_cert_ignore_2(self):
t_default.build_session()
self.assertIs(False, t_default.session.verify)
+ # TODO: I am not sure in which version changed specifically, but this can be updated if we need to find out.
+ @unittest.skipIf(StrictVersion(requests.__version__) > StrictVersion('2.9.1'), reason="Skipping for versions 2.9.1 or older")
+ def test_build_session_proxy_none_old_request(self):
+ os.environ['HTTP_PROXY'] = 'random_proxy'
+ os.environ['HTTPS_PROXY'] = 'random_proxy_2'
+
+ t_default = transport.Transport(endpoint="https://example.com",
+ server_cert_validation='validate',
+ username='test',
+ password='test',
+ auth_method='basic',
+ proxy=None
+ )
+
+ t_default.build_session()
+ self.assertEqual({'no_proxy': '*', 'http': 'random_proxy', 'https': 'random_proxy_2'}, t_default.session.proxies)
+
+ @unittest.skipIf(StrictVersion(requests.__version__) <= StrictVersion('2.9.1'), reason="Skipping for versions newer than 2.9.1")
def test_build_session_proxy_none(self):
os.environ['HTTP_PROXY'] = 'random_proxy'
os.environ['HTTPS_PROXY'] = 'random_proxy_2'
@@ -308,3 +313,49 @@ def test_close_session_not_built(self, mock_session):
t_default.close_session()
self.assertFalse(mock_session.return_value.close.called)
self.assertIsNone(t_default.session)
+
+
+class TestTransportCredSSP(base_test.BaseTest):
+
+ @unittest.skipIf(base_test.EXPECT_CREDSSP is False, reason="Only testing when CredSSP is available")
+ def test_with_credssp(self):
+ t_default = transport.Transport(endpoint="https://example.com",
+ username='test',
+ password='test',
+ auth_method='credssp',
+ )
+ t_default.build_session()
+
+ @unittest.skipIf(base_test.EXPECT_CREDSSP is True, reason="Only testing when CredSSP is unavailable")
+ def test_without_credssp(self):
+ t_default = transport.Transport(endpoint="https://example.com",
+ username='test',
+ password='test',
+ auth_method='credssp',
+ )
+ with self.assertRaises(WinRMError) as exc:
+ t_default.build_session()
+ self.assertEqual(str(exc.exception), 'requests auth method is credssp, but requests-credssp is not installed')
+
+
+class TestTransportKerberos(base_test.BaseTest):
+
+ @unittest.skipIf(base_test.EXPECT_KERBEROS is False, reason="Only testing when kerberos is available")
+ def test_with_kerberos(self):
+ t_default = transport.Transport(endpoint="https://example.com",
+ username='test',
+ password='test',
+ auth_method='kerberos',
+ )
+ t_default.build_session()
+
+ @unittest.skipIf(base_test.EXPECT_KERBEROS is True, reason="Only testing when kerberos is unavailable")
+ def test_without_kerberos(self):
+ t_default = transport.Transport(endpoint="https://example.com",
+ username='test',
+ password='test',
+ auth_method='kerberos',
+ )
+ with self.assertRaises(WinRMError) as exc:
+ t_default.build_session()
+ self.assertEqual(str(exc.exception), 'requested auth method is kerberos, but requests_kerberos is not installed')
diff --git a/winrm/tests/winrm_responses/__init__.py b/winrm/tests/winrm_responses/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/winrm/tests/winrm_responses/shells.py b/winrm/tests/winrm_responses/shells.py
new file mode 100644
index 00000000..bf24353d
--- /dev/null
+++ b/winrm/tests/winrm_responses/shells.py
@@ -0,0 +1,147 @@
+OPEN_SHELL_REQUEST = """
+
+
+ PT20S
+ http://windows-host:5985/wsman
+
+ FALSE
+ 437
+
+ 153600
+ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd
+ http://schemas.xmlsoap.org/ws/2004/09/transfer/Create
+
+
+ http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
+
+ uuid:63acc10b-c16f-44e6-a1f8-45798c58f63d
+
+
+
+
+ stdin
+ stdout stderr
+
+
+
+"""
+
+OPEN_SHELL_RESPONSE = """
+
+
+ http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse
+ uuid:BF99E0E5-4604-4FBE-A3F9-3C2251E2655E
+ http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
+ uuid:63acc10b-c16f-44e6-a1f8-45798c58f63d
+
+
+
+ http://windows-host:5985/wsman
+
+ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd
+
+ 5207F2DF-E6CA-4D10-8C7F-5380F01D6FDE
+
+
+
+
+ 5207F2DF-E6CA-4D10-8C7F-5380F01D6FDE
+ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd
+ SERVER-01\\Administrator
+ 192.168.0.1
+ PT7200.000S
+ stdin
+ stdout stderr
+ P0DT0H0M0S
+ P0DT0H0M0S
+
+
+
+"""
+
+CLOSE_SHELL_REQUEST = """
+
+
+
+ PT20S
+ http://windows-host:5985/wsman
+
+ B5235B70-E451-4378-BFB4-53C1ABACCAD2
+
+ 153600
+ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd
+ http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete
+
+
+ http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
+
+ uuid:5fc69899-bbc7-4179-a7a2-f6bb62fe2860
+
+
+
+
+"""
+
+CLOSE_SHELL_RESPONSE = """
+
+
+ http://schemas.xmlsoap.org/ws/2004/09/transfer/DeleteResponse
+ uuid:C20C16C3-D163-45B4-9821-8A11C4ED1E42
+ http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
+ uuid:5fc69899-bbc7-4179-a7a2-f6bb62fe2860
+
+
+
+"""
+
+# Status code: 500
+CLOSE_SHELL_RESPONSE_ALREADY_CLOSED = """
+
+
+ http://schemas.dmtf.org/wbem/wsman/1/wsman/fault
+ uuid:C0005351-7407-4B75-BE76-2A80BA45B7BF
+ http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
+ uuid:fe945306-d7ce-4126-b168-c704bad7588c
+
+
+
+
+ s:Sender
+
+ w:InvalidSelectors
+
+
+
+ The WS-Management service cannot process the request because the request contained invalid selectors for the
+ resource.
+
+
+
+ http://schemas.dmtf.org/wbem/wsman/1/wsman/faultDetail/UnexpectedSelectors
+
+ The request for the Windows Remote Shell with ShellId B387C9DF-58ED-4E97-AE80-4117D8D12116 failed because the shell was not found
+ on the server. Possible causes are: the specified ShellId is incorrect or the shell no longer exists on the server. Provide the correct
+ ShellId or create a new shell and retry the operation.
+
+
+
+
+
+
+"""