Repository: softlayer/softlayer-python (python)

Issue:
Add "verify" as an option to create_client_from_env and the XMLRPC and REST transports
### Expected Behavior
This code should now stand up a transport where `verify=False` (by default) for each request.
```python
client = SoftLayer.create_client_from_env(verify=False)
account = client.call('SoftLayer', 'getObject') # no, don't verify
account = client.call('SoftLayer', 'getObject', verify=True)  # yes, verify
account = client.call('SoftLayer', 'getObject', verify=False) # no, don't verify

# This should default to verifying each request:
client = SoftLayer.create_client_from_env()
account = client.call('SoftLayer', 'getObject')  # yes, verify
account = client.call('SoftLayer', 'getObject', verify=False) # no, don't verify
account = client.call('SoftLayer', 'getObject', verify=True) # yes, verify
```


Pull request description:
Adds verify option to create_client_from_env
Resolves #772

Patch:
diff --git a/SoftLayer/API.py b/SoftLayer/API.py
index 322309f2..2a79264c 100644
--- a/SoftLayer/API.py
+++ b/SoftLayer/API.py
@@ -46,7 +46,8 @@ def create_client_from_env(username=None,
                            config_file=None,
                            proxy=None,
                            user_agent=None,
-                           transport=None):
+                           transport=None,
+                           verify=True):
     """Creates a SoftLayer API client using your environment.
 
     Settings are loaded via keyword arguments, environemtal variables and
@@ -68,6 +69,8 @@ def create_client_from_env(username=None,
         calls if you wish to bypass the packages built in User Agent string
     :param transport: An object that's callable with this signature:
                       transport(SoftLayer.transports.Request)
+    :param bool verify: decide to verify the server's SSL/TLS cert. DO NOT SET
+                        TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS.
 
     Usage:
 
@@ -83,6 +86,7 @@ def create_client_from_env(username=None,
                                           endpoint_url=endpoint_url,
                                           timeout=timeout,
                                           proxy=proxy,
+                                          verify=verify,
                                           config_file=config_file)
 
     if transport is None:
@@ -94,6 +98,7 @@ def create_client_from_env(username=None,
                 proxy=settings.get('proxy'),
                 timeout=settings.get('timeout'),
                 user_agent=user_agent,
+                verify=verify,
             )
         else:
             # Default the transport to use XMLRPC
@@ -102,6 +107,7 @@ def create_client_from_env(username=None,
                 proxy=settings.get('proxy'),
                 timeout=settings.get('timeout'),
                 user_agent=user_agent,
+                verify=verify,
             )
 
     # If we have enough information to make an auth driver, let's do it
@@ -240,7 +246,8 @@ def call(self, service, method, *args, **kwargs):
         request.filter = kwargs.get('filter')
         request.limit = kwargs.get('limit')
         request.offset = kwargs.get('offset')
-        request.verify = kwargs.get('verify')
+        if kwargs.get('verify') is not None:
+            request.verify = kwargs.get('verify')
 
         if self.auth:
             extra_headers = self.auth.get_headers()
diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py
index 8e143818..b3239fc1 100644
--- a/SoftLayer/transports.py
+++ b/SoftLayer/transports.py
@@ -64,7 +64,7 @@ def __init__(self):
         self.transport_headers = {}
 
         #: Boolean specifying if the server certificate should be verified.
-        self.verify = True
+        self.verify = None
 
         #: Client certificate file path.
         self.cert = None
@@ -103,13 +103,15 @@ def __init__(self,
                  endpoint_url=None,
                  timeout=None,
                  proxy=None,
-                 user_agent=None):
+                 user_agent=None,
+                 verify=True):
 
         self.endpoint_url = (endpoint_url or
                              consts.API_PUBLIC_ENDPOINT).rstrip('/')
         self.timeout = timeout or None
         self.proxy = proxy
         self.user_agent = user_agent or consts.USER_AGENT
+        self.verify = verify
 
     def __call__(self, request):
         """Makes a SoftLayer API call against the XML-RPC endpoint.
@@ -145,6 +147,12 @@ def __call__(self, request):
         payload = utils.xmlrpc_client.dumps(tuple(largs),
                                             methodname=request.method,
                                             allow_none=True)
+
+        # Prefer the request setting, if it's not None
+        verify = request.verify
+        if verify is None:
+            verify = self.verify
+
         LOGGER.debug("=== REQUEST ===")
         LOGGER.info('POST %s', url)
         LOGGER.debug(request.transport_headers)
@@ -155,7 +163,7 @@ def __call__(self, request):
                                     data=payload,
                                     headers=request.transport_headers,
                                     timeout=self.timeout,
-                                    verify=request.verify,
+                                    verify=verify,
                                     cert=request.cert,
                                     proxies=_proxies_dict(self.proxy))
             LOGGER.debug("=== RESPONSE ===")
@@ -202,13 +210,15 @@ def __init__(self,
                  endpoint_url=None,
                  timeout=None,
                  proxy=None,
-                 user_agent=None):
+                 user_agent=None,
+                 verify=True):
 
         self.endpoint_url = (endpoint_url or
                              consts.API_PUBLIC_ENDPOINT_REST).rstrip('/')
         self.timeout = timeout or None
         self.proxy = proxy
         self.user_agent = user_agent or consts.USER_AGENT
+        self.verify = verify
 
     def __call__(self, request):
         """Makes a SoftLayer API call against the REST endpoint.
@@ -269,6 +279,11 @@ def __call__(self, request):
 
         url = '%s.%s' % ('/'.join(url_parts), 'json')
 
+        # Prefer the request setting, if it's not None
+        verify = request.verify
+        if verify is None:
+            verify = self.verify
+
         LOGGER.debug("=== REQUEST ===")
         LOGGER.info(url)
         LOGGER.debug(request.transport_headers)
@@ -280,14 +295,14 @@ def __call__(self, request):
                                     params=params,
                                     data=raw_body,
                                     timeout=self.timeout,
-                                    verify=request.verify,
+                                    verify=verify,
                                     cert=request.cert,
                                     proxies=_proxies_dict(self.proxy))
             LOGGER.debug("=== RESPONSE ===")
             LOGGER.debug(resp.headers)
-            LOGGER.debug(resp.content)
+            LOGGER.debug(resp.text)
             resp.raise_for_status()
-            result = json.loads(resp.content)
+            result = json.loads(resp.text)
 
             if isinstance(result, list):
                 return SoftLayerListResult(
@@ -295,9 +310,9 @@ def __call__(self, request):
             else:
                 return result
         except requests.HTTPError as ex:
-            content = json.loads(ex.response.content)
+            message = json.loads(ex.response.text)['error']
             raise exceptions.SoftLayerAPIError(ex.response.status_code,
-                                               content['error'])
+                                               message)
         except requests.RequestException as ex:
             raise exceptions.TransportError(0, str(ex))
 
diff --git a/setup.cfg b/setup.cfg
index 20520b55..ba4e6f12 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,4 +1,4 @@
-[pytest]
+[tool:pytest]
 python_files = *_tests.py
 
 [wheel]


Test patch:
diff --git a/tests/api_tests.py b/tests/api_tests.py
index ff5fce60..db42ee35 100644
--- a/tests/api_tests.py
+++ b/tests/api_tests.py
@@ -16,7 +16,8 @@ class Inititialization(testing.TestCase):
     def test_init(self):
         client = SoftLayer.Client(username='doesnotexist',
                                   api_key='issurelywrong',
-                                  timeout=10)
+                                  timeout=10,
+                                  endpoint_url='http://example.com/v3/xmlrpc/')
 
         self.assertIsInstance(client.auth, SoftLayer.BasicAuthentication)
         self.assertEqual(client.auth.username, 'doesnotexist')
@@ -95,7 +96,7 @@ def test_simple_call(self):
                                 offset=None,
                                 )
 
-    def test_verify(self):
+    def test_verify_request_false(self):
         client = SoftLayer.BaseClient(transport=self.mocks)
         mock = self.set_mock('SoftLayer_SERVICE', 'METHOD')
         mock.return_value = {"test": "result"}
@@ -105,6 +106,26 @@ def test_verify(self):
         self.assertEqual(resp, {"test": "result"})
         self.assert_called_with('SoftLayer_SERVICE', 'METHOD', verify=False)
 
+    def test_verify_request_true(self):
+        client = SoftLayer.BaseClient(transport=self.mocks)
+        mock = self.set_mock('SoftLayer_SERVICE', 'METHOD')
+        mock.return_value = {"test": "result"}
+
+        resp = client.call('SERVICE', 'METHOD', verify=True)
+
+        self.assertEqual(resp, {"test": "result"})
+        self.assert_called_with('SoftLayer_SERVICE', 'METHOD', verify=True)
+
+    def test_verify_request_not_specified(self):
+        client = SoftLayer.BaseClient(transport=self.mocks)
+        mock = self.set_mock('SoftLayer_SERVICE', 'METHOD')
+        mock.return_value = {"test": "result"}
+
+        resp = client.call('SERVICE', 'METHOD')
+
+        self.assertEqual(resp, {"test": "result"})
+        self.assert_called_with('SoftLayer_SERVICE', 'METHOD', verify=None)
+
     @mock.patch('SoftLayer.API.BaseClient.iter_call')
     def test_iterate(self, _iter_call):
         self.client['SERVICE'].METHOD(iter=True)
diff --git a/tests/transport_tests.py b/tests/transport_tests.py
index e262b7eb..690882f6 100644
--- a/tests/transport_tests.py
+++ b/tests/transport_tests.py
@@ -8,6 +8,7 @@
 import warnings
 
 import mock
+import pytest
 import requests
 import six
 
@@ -17,26 +18,31 @@
 from SoftLayer import transports
 
 
+def get_xmlrpc_response():
+    response = requests.Response()
+    list_body = six.b('''<?xml version="1.0" encoding="utf-8"?>
+<params>
+<param>
+<value>
+<array>
+<data/>
+</array>
+</value>
+</param>
+</params>''')
+    response.raw = io.BytesIO(list_body)
+    response.headers['SoftLayer-Total-Items'] = 10
+    response.status_code = 200
+    return response
+
+
 class TestXmlRpcAPICall(testing.TestCase):
 
     def set_up(self):
         self.transport = transports.XmlRpcTransport(
             endpoint_url='http://something.com',
         )
-        self.response = requests.Response()
-        list_body = six.b('''<?xml version="1.0" encoding="utf-8"?>
-<params>
-<param>
- <value>
-  <array>
-   <data/>
-  </array>
- </value>
-</param>
-</params>''')
-        self.response.raw = io.BytesIO(list_body)
-        self.response.headers['SoftLayer-Total-Items'] = 10
-        self.response.status_code = 200
+        self.response = get_xmlrpc_response()
 
     @mock.patch('requests.request')
     def test_call(self, request):
@@ -251,6 +257,55 @@ def test_request_exception(self, request):
         self.assertRaises(SoftLayer.TransportError, self.transport, req)
 
 
+@mock.patch('requests.request')
+@pytest.mark.parametrize(
+    "transport_verify,request_verify,expected",
+    [
+        (True, True, True),
+        (True, False, False),
+        (True, None, True),
+
+        (False, True, True),
+        (False, False, False),
+        (False, None, False),
+
+        (None, True, True),
+        (None, False, False),
+        (None, None, True),
+    ]
+)
+def test_verify(request,
+                transport_verify,
+                request_verify,
+                expected):
+    request.return_value = get_xmlrpc_response()
+
+    transport = transports.XmlRpcTransport(
+        endpoint_url='http://something.com',
+    )
+
+    req = transports.Request()
+    req.service = 'SoftLayer_Service'
+    req.method = 'getObject'
+
+    if request_verify is not None:
+        req.verify = request_verify
+
+    if transport_verify is not None:
+        transport.verify = transport_verify
+
+    transport(req)
+
+    request.assert_called_with('POST',
+                               'http://something.com/SoftLayer_Service',
+                               data=mock.ANY,
+                               headers=mock.ANY,
+                               cert=mock.ANY,
+                               proxies=mock.ANY,
+                               timeout=mock.ANY,
+                               verify=expected)
+
+
 class TestRestAPICall(testing.TestCase):
 
     def set_up(self):
@@ -261,6 +316,7 @@ def set_up(self):
     @mock.patch('requests.request')
     def test_basic(self, request):
         request().content = '[]'
+        request().text = '[]'
         request().headers = requests.structures.CaseInsensitiveDict({
             'SoftLayer-Total-Items': '10',
         })
@@ -290,7 +346,7 @@ def test_error(self, request):
         e = requests.HTTPError('error')
         e.response = mock.MagicMock()
         e.response.status_code = 404
-        e.response.content = '''{
+        e.response.text = '''{
             "error": "description",
             "code": "Error Code"
         }'''
@@ -315,7 +371,7 @@ def test_proxy_without_protocol(self):
 
     @mock.patch('requests.request')
     def test_valid_proxy(self, request):
-        request().content = '{}'
+        request().text = '{}'
         self.transport.proxy = 'http://localhost:3128'
 
         req = transports.Request()
@@ -323,6 +379,7 @@ def test_valid_proxy(self, request):
         req.method = 'Resource'
 
         self.transport(req)
+
         request.assert_called_with(
             'GET', 'http://something.com/SoftLayer_Service/Resource.json',
             proxies={'https': 'http://localhost:3128',
@@ -337,7 +394,7 @@ def test_valid_proxy(self, request):
 
     @mock.patch('requests.request')
     def test_with_id(self, request):
-        request().content = '{}'
+        request().text = '{}'
 
         req = transports.Request()
         req.service = 'SoftLayer_Service'
@@ -361,7 +418,7 @@ def test_with_id(self, request):
 
     @mock.patch('requests.request')
     def test_with_args(self, request):
-        request().content = '{}'
+        request().text = '{}'
 
         req = transports.Request()
         req.service = 'SoftLayer_Service'
@@ -385,7 +442,7 @@ def test_with_args(self, request):
 
     @mock.patch('requests.request')
     def test_with_filter(self, request):
-        request().content = '{}'
+        request().text = '{}'
 
         req = transports.Request()
         req.service = 'SoftLayer_Service'
@@ -410,7 +467,7 @@ def test_with_filter(self, request):
 
     @mock.patch('requests.request')
     def test_with_mask(self, request):
-        request().content = '{}'
+        request().text = '{}'
 
         req = transports.Request()
         req.service = 'SoftLayer_Service'