Repository: digitalocean/pynetbox
Pull request #41 (2018-03-07 19:40:31)

SSL verification control for #21
See the prior PR https://github.com/digitalocean/pynetbox/pull/23  which included unrelated other features.  This is the SSL part with the other stuff removed.  I'll forego re-describing everything here :)

I'm not sure if I covered the testing specifically requested, but I'm happy to add more if desired.


Patch:
diff --git a/.gitignore b/.gitignore
index 031771ed..c5de574 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,8 +14,8 @@ dist/
 downloads/
 eggs/
 .eggs/
-lib/
 lib64/
+lib/
 parts/
 sdist/
 var/
diff --git a/pynetbox/VERSION b/pynetbox/VERSION
index fd2a018..bea438e 100644
--- a/pynetbox/VERSION
+++ b/pynetbox/VERSION
@@ -1,1 +1,1 @@
-3.1.0
+3.3.1
diff --git a/pynetbox/api.py b/pynetbox/api.py
index 546390e..0b0cf9d 100644
--- a/pynetbox/api.py
+++ b/pynetbox/api.py
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 '''
 from pynetbox.lib import Endpoint, Request
-from pynetbox import dcim, ipam
+from pynetbox import dcim, ipam, virtualization
 
 
 class App(object):
@@ -56,6 +56,7 @@ class Api(object):
     Calling any of these attributes will return
     :py:class:`.App` which exposes endpoints as attributes.
 
+    :type ssl_verify: bool or str
     :param str url: The base url to the instance of Netbox you
         wish to connect to.
     :param str token: Your netbox token.
@@ -64,6 +65,8 @@ class Api(object):
     :param str,optional private_key: Your private key.
     :param str,optional version: Override the API version, otherwise
         it's dynamically discovered.
+    :param bool or str,optional ssl_verify: Specify SSL verification behavior
+        see: http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification
     :raises ValueError: If *private_key* and *private_key_file* are both
         specified.
     :raises AttributeError: If app doesn't exist.
@@ -77,10 +80,10 @@ class Api(object):
     ... )
     >>> nb.dcim.devices.all()
 
-    """
+    """  # noqa
 
     def __init__(self, url, token=None, private_key=None,
-                 private_key_file=None, version=None):
+                 private_key_file=None, version=None, ssl_verify=True):
         if private_key and private_key_file:
             raise ValueError(
                 '"private_key" and "private_key_file" cannot be used together.'
@@ -92,6 +95,7 @@ class Api(object):
             "private_key": private_key,
             "private_key_file": private_key_file,
             "base_url": base_url,
+            "ssl_verify": ssl_verify,
         }
 
         if self.api_kwargs.get('private_key_file'):
@@ -99,13 +103,15 @@ class Api(object):
                 private_key = kf.read()
                 self.api_kwargs.update(private_key=private_key)
         if not version:
-            version = Request(base=base_url).get_version()
+            version = Request(base=base_url,
+                              ssl_verify=ssl_verify).get_version()
         self.api_kwargs.update(version=version)
 
         req = Request(
             base=base_url,
             token=token,
-            private_key=private_key
+            private_key=private_key,
+            ssl_verify=ssl_verify
         )
         if token and private_key:
             self.api_kwargs.update(session_key=req.get_session_key())
@@ -116,4 +122,4 @@ class Api(object):
         self.secrets = App('secrets', api_kwargs=self.api_kwargs)
         self.tenancy = App('tenancy', api_kwargs=self.api_kwargs)
         self.extras = App('extras', api_kwargs=self.api_kwargs)
-        self.virtualization = App('virtualization', api_kwargs=self.api_kwargs)
+        self.virtualization = App(virtualization, api_kwargs=self.api_kwargs)
diff --git a/pynetbox/dcim.py b/pynetbox/dcim.py
index b72e486..bb81f05 100644
--- a/pynetbox/dcim.py
+++ b/pynetbox/dcim.py
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 '''
 from pynetbox.lib.response import Record
+from pynetbox.lib.endpoint import RODetailEndpoint
 from pynetbox.ipam import IpAddresses
 
 
@@ -43,6 +44,27 @@ class Devices(Record):
     primary_ip4 = IpAddresses
     primary_ip6 = IpAddresses
 
+    @property
+    def napalm(self):
+        """ Represents the ``napalm`` detail endpoint.
+
+        Returns a DetailEndpoint object that is the interface for
+        viewing response from the napalm endpoint.
+
+        :returns: :py:class:`.DetailEndpoint`
+
+        :Examples:
+
+        >>> device = nb.ipam.devices.get(123)
+        >>> device.napalm.list(method='get_facts')
+        {"get_facts": {"interface_list": ["ge-0/0/0"]}}
+
+        """
+        return RODetailEndpoint(
+            'napalm',
+            parent_obj=self,
+        )
+
 
 class InterfaceConnections(Record):
 
diff --git a/pynetbox/lib/endpoint.py b/pynetbox/lib/endpoint.py
index 7ab294d..029df83 100644
--- a/pynetbox/lib/endpoint.py
+++ b/pynetbox/lib/endpoint.py
@@ -15,7 +15,7 @@ limitations under the License.
 '''
 from collections import defaultdict
 
-from pynetbox.lib.query import Request
+from pynetbox.lib.query import Request, url_param_builder
 from pynetbox.lib.response import Record, IPRecord
 
 CACHE = defaultdict(list)
@@ -56,6 +56,7 @@ class Endpoint(object):
         self.token = api_kwargs.get('token')
         self.version = api_kwargs.get('version')
         self.session_key = api_kwargs.get('session_key')
+        self.ssl_verify = api_kwargs.get('ssl_verify')
         self.url = '{base_url}/{app}/{endpoint}'.format(
             base_url=self.base_url,
             app=app_name,
@@ -109,6 +110,7 @@ class Endpoint(object):
             token=self.token,
             session_key=self.session_key,
             version=self.version,
+            ssl_verify=self.ssl_verify,
         )
         ret_kwargs = dict(
             api_kwargs=self.api_kwargs,
@@ -165,7 +167,8 @@ class Endpoint(object):
             base=self.url,
             token=self.token,
             session_key=self.session_key,
-            version=self.version
+            version=self.version,
+            ssl_verify=self.ssl_verify,
         )
         ret_kwargs = dict(
             api_kwargs=self.api_kwargs,
@@ -233,6 +236,7 @@ class Endpoint(object):
             token=self.token,
             session_key=self.session_key,
             version=self.version,
+            ssl_verify=self.ssl_verify,
         )
         ret_kwargs = dict(
             api_kwargs=self.api_kwargs,
@@ -301,6 +305,7 @@ class Endpoint(object):
             token=self.token,
             session_key=self.session_key,
             version=self.version,
+            ssl_verify=self.ssl_verify,
         ).post(args[0] if len(args) > 0 else kwargs)
 
 
@@ -315,6 +320,7 @@ class DetailEndpoint(object):
         self.token = parent_obj.api_kwargs.get('token')
         self.version = parent_obj.api_kwargs.get('version')
         self.session_key = parent_obj.api_kwargs.get('session_key')
+        self.ssl_verify = parent_obj.api_kwargs.get('ssl_verify')
         self.url = "{}/{}/{}/".format(
             parent_obj.endpoint_meta.get('url'),
             parent_obj.id,
@@ -325,16 +331,28 @@ class DetailEndpoint(object):
             token=self.token,
             session_key=self.session_key,
             version=self.version,
+            ssl_verify=self.ssl_verify,
         )
 
-    def list(self):
+    def list(self, **kwargs):
         """The view operation for a detail endpoint
 
         Returns the response from NetBox for a detail endpoint.
 
+        :args \**kwargs: key/value pairs that get converted into url
+            parameters when passed to the endpoint.
+            E.g. ``.list(method='get_facts')`` would be converted to
+            ``.../?method=get_facts``.
+
         :returns: A dictionary or list of dictionaries its retrieved
             from NetBox.
         """
+        if kwargs:
+            self.request_kwargs['base'] = '{}{}'.format(
+                self.url,
+                url_param_builder(kwargs)
+            )
+
         return Request(**self.request_kwargs).get()
 
     def create(self, data={}):
@@ -351,3 +369,11 @@ class DetailEndpoint(object):
             NetBox.
         """
         return Request(**self.request_kwargs).post(data)
+
+
+class RODetailEndpoint(DetailEndpoint):
+
+    def create(self, data={}):
+        raise NotImplementedError(
+            'Writes are not supported for this endpoint.'
+        )
diff --git a/pynetbox/lib/query.py b/pynetbox/lib/query.py
index 7663bef..6d93613 100644
--- a/pynetbox/lib/query.py
+++ b/pynetbox/lib/query.py
@@ -19,6 +19,15 @@ from six.moves.urllib.parse import urlencode
 import requests
 
 
+def url_param_builder(param_dict):
+    '''Builds url parameters
+
+    Creates URL paramters (e.g. '.../?xyz=r21&abc=123') from a dict
+    passed in param_dict
+    '''
+    return '?{}'.format(urlencode(param_dict))
+
+
 class RequestError(Exception):
     """Basic Request Exception
 
@@ -67,7 +76,8 @@ class Request(object):
     """
 
     def __init__(self, base=None, filters=None, key=None, token=None,
-                 private_key=None, version=None, session_key=None):
+                 private_key=None, version=None, session_key=None,
+                 ssl_verify=True):
         """
         Instantiates a new Request object
 
@@ -88,6 +98,7 @@ class Request(object):
         self.private_key = private_key
         self.version = version
         self.session_key = session_key
+        self.ssl_verify = ssl_verify
 
     def get_version(self):
         """Query the netbox API for its API-Version.
@@ -97,7 +108,8 @@ class Request(object):
         :returns: String containing version.
         """
         ret = requests.get(
-            "{}/".format(self.base)
+            "{}/".format(self.base),
+            verify=self.ssl_verify
         ).headers.get('API-Version', '1.0')
         setattr(self, 'version', ret)
         return ret
@@ -119,7 +131,8 @@ class Request(object):
             },
             data=urlencode({
                 'private_key': self.private_key.strip('\n')
-            })
+            }),
+            verify=self.ssl_verify
         )
         if req.ok:
             return json.loads(req.text)['session_key']
@@ -138,9 +151,9 @@ class Request(object):
             for k, v in input.items():
                 if isinstance(v, list):
                     for i in v:
-                        yield "{}={}".format(k, i)
+                        yield urlencode({k: i})
                 else:
-                    yield "{}={}".format(k, v)
+                    yield urlencode({k: v})
 
         if self.key:
             return '{}/{key}/'.format(
@@ -199,7 +212,7 @@ class Request(object):
 
         def make_request(url):
 
-            req = requests.get(url, headers=headers)
+            req = requests.get(url, headers=headers, verify=self.ssl_verify)
             if req.ok:
                 return json.loads(req.text)
             else:
@@ -247,7 +260,10 @@ class Request(object):
             headers.update(
                 {'X-Session-Key': self.session_key}
             )
-        req = requests.put(self.url, headers=headers, data=json.dumps(data))
+        req = requests.put(self.url,
+                           headers=headers,
+                           data=json.dumps(data),
+                           verify=self.ssl_verify)
         if req.ok:
             return json.loads(req.text)
         else:
@@ -277,7 +293,8 @@ class Request(object):
         req = requests.post(
             self.normalize_url(self.url),
             headers=headers,
-            data=json.dumps(data)
+            data=json.dumps(data),
+            verify=self.ssl_verify
         )
         if req.ok:
             return json.loads(req.text)
@@ -305,6 +322,7 @@ class Request(object):
         req = requests.delete(
             "{}".format(self.url),
             headers=headers,
+            verify=self.ssl_verify
         )
         if req.ok:
             return True
diff --git a/pynetbox/lib/response.py b/pynetbox/lib/response.py
index 5e2dfaf..c1bab23 100644
--- a/pynetbox/lib/response.py
+++ b/pynetbox/lib/response.py
@@ -173,7 +173,8 @@ class Record(object):
                 base=self.url,
                 token=self.api_kwargs.get('token'),
                 session_key=self.api_kwargs.get('session_key'),
-                version=self.api_kwargs.get('version')
+                version=self.api_kwargs.get('version'),
+                ssl_verify=self.api_kwargs.get('ssl_verify')
             )
             self._parse_values(req.get())
             self.has_details = True
@@ -230,7 +231,8 @@ class Record(object):
                     base=self.endpoint_meta.get('url'),
                     token=self.api_kwargs.get('token'),
                     session_key=self.api_kwargs.get('session_key'),
-                    version=self.api_kwargs.get('version')
+                    version=self.api_kwargs.get('version'),
+                    ssl_verify=self.api_kwargs.get('ssl_verify')
                 )
                 if req.put(self.serialize()):
                     return True
@@ -253,7 +255,8 @@ class Record(object):
             base=self.endpoint_meta.get('url'),
             token=self.api_kwargs.get('token'),
             session_key=self.api_kwargs.get('session_key'),
-            version=self.api_kwargs.get('version')
+            version=self.api_kwargs.get('version'),
+            ssl_verify=self.api_kwargs.get('ssl_verify')
         )
         if req.delete():
             return True
diff --git a/pynetbox/virtualization.py b/pynetbox/virtualization.py
new file mode 100644
index 0000000..e2f5328
--- /dev/null
+++ b/pynetbox/virtualization.py
@@ -0,0 +1,24 @@
+'''
+(c) 2017 DigitalOcean
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+'''
+from pynetbox.lib.response import Record
+from pynetbox.ipam import IpAddresses
+
+
+class VirtualMachines(Record):
+
+    primary_ip = IpAddresses
+    primary_ip4 = IpAddresses
+    primary_ip6 = IpAddresses


Test patch:
diff --git a/tests/fixtures/dcim/napalm.json b/tests/fixtures/dcim/napalm.json
new file mode 100644
index 0000000..1557906
--- /dev/null
+++ b/tests/fixtures/dcim/napalm.json
@@ -0,0 +1,14 @@
+{
+    "get_facts": {
+        "interface_list": [
+            "xe-0/0/0"
+        ],
+        "serial_number": "ABC!@#",
+        "vendor": "PacketPusher",
+        "os_version": "1.1",
+        "hostname": "test-1",
+        "model": "UnobtaniumX",
+        "fqdn": "None",
+        "uptime": 2
+    }
+}
\ No newline at end of file
diff --git a/tests/test_api.py b/tests/test_api.py
index 7dee834..85cc36f 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -9,17 +9,71 @@ if six.PY3:
 else:
     from mock import patch
 
+host = "http://localhost:8000"
+
+def_kwargs = {
+    'token': 'abc123',
+    'private_key_file': 'tests/fixtures/api/get_session_key.json',
+    'version': '2.0'
+}
+
+# Keys are app names, values are arbitrarily selected endpoints
+# We use dcim and ipam since they have unique app classes
+# and circuits because it does not. We don't add other apps/endpoints
+# beyond 'circuits' as they all use the same code as each other
+endpoints = {
+    'dcim': 'devices',
+    'ipam': 'prefixes',
+    'circuits': 'circuits',
+}
 
 class ApiTestCase(unittest.TestCase):
+
     @patch(
         'pynetbox.lib.query.requests.post',
         return_value=Response(fixture='api/get_session_key.json')
     )
     def test_get(self, mock):
         api = pynetbox.api(
-            "http://localhost:8000",
-            token='abc123',
-            private_key_file='tests/fixtures/api/get_session_key.json',
-            version='2.0'
+            host,
+            **def_kwargs
         )
         self.assertTrue(api)
+
+
+class ApiArgumentsTestCase(unittest.TestCase):
+
+    @patch(
+        'pynetbox.lib.query.requests.post',
+        return_value=Response(fixture='api/get_session_key.json')
+    )
+    def common_arguments(self, kwargs, arg, expect, mock):
+        '''
+        Ensures the api and endpoint instances have ssl_verify set
+        as expected
+        '''
+        api = pynetbox.api(
+            host,
+            **kwargs
+        )
+        self.assertIs(api.api_kwargs.get(arg, "fail"), expect)
+        for app, endpoint in endpoints.items():
+            ep = getattr(getattr(api, app), endpoint)
+            self.assertIs(getattr(ep, arg), expect)
+
+    def test_ssl_verify_default(self):
+        self.common_arguments(def_kwargs, 'ssl_verify', True)
+
+    def test_ssl_verify_true(self):
+        kwargs = dict(def_kwargs, **{'ssl_verify': True})
+        self.common_arguments(kwargs, 'ssl_verify', True)
+
+    def test_ssl_verify_false(self):
+        kwargs = dict(def_kwargs, **{'ssl_verify': False})
+        self.common_arguments(kwargs, 'ssl_verify', False)
+
+    def test_ssl_verify_string(self):
+        kwargs = dict(def_kwargs, **{'ssl_verify': '/path/to/bundle'})
+        self.common_arguments(kwargs, 'ssl_verify', '/path/to/bundle')
+
+
diff --git a/tests/test_circuits.py b/tests/test_circuits.py
index 44e4295..6d03692 100644
--- a/tests/test_circuits.py
+++ b/tests/test_circuits.py
@@ -43,7 +43,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_filter(self):
@@ -63,7 +64,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_get(self):
@@ -82,7 +84,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
 
diff --git a/tests/test_dcim.py b/tests/test_dcim.py
index db9b173..e291ab4 100644
--- a/tests/test_dcim.py
+++ b/tests/test_dcim.py
@@ -49,7 +49,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_filter(self):
@@ -69,7 +70,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_get(self):
@@ -90,7 +92,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_delete(self):
@@ -108,14 +111,16 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
             delete.assert_called_with(
                 'http://localhost:8000/api/{}/{}/1/'.format(
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=AUTH_HEADERS
+                headers=AUTH_HEADERS,
+                verify=True
             )
 
     def test_compare(self):
@@ -161,7 +166,8 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
                 self.app,
                 self.name.replace('_', '-')
             ),
-            headers=HEADERS
+            headers=HEADERS,
+            verify=True
         )
 
     @patch(
@@ -169,16 +175,17 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
         return_value=Response(fixture='dcim/devices.json')
     )
     def test_multi_filter(self, mock):
-        ret = getattr(nb, self.name).filter(role=['test', 'test1'], site='TEST1')
+        ret = getattr(nb, self.name).filter(role=['test', 'test1'], site='TEST#1')
         self.assertTrue(ret)
         self.assertTrue(isinstance(ret, list))
         self.assertTrue(isinstance(ret[0], self.ret))
         mock.assert_called_with(
-            'http://localhost:8000/api/{}/{}/?role=test&role=test1&site=TEST1'.format(
+            'http://localhost:8000/api/{}/{}/?role=test&role=test1&site=TEST%231'.format(
                 self.app,
                 self.name.replace('_', '-')
             ),
-            headers=HEADERS
+            headers=HEADERS,
+            verify=True
         )
 
     @patch(
@@ -249,6 +256,23 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
             self.ret
         ))
 
+    @patch(
+        'pynetbox.lib.query.requests.get',
+        side_effect=[
+            Response(fixture='dcim/device.json'),
+            Response(fixture='dcim/napalm.json'),
+        ]
+    )
+    def test_get_napalm(self, mock):
+        test = nb.devices.get(1)
+        ret = test.napalm.list(method='get_facts')
+        mock.assert_called_with(
+            'http://localhost:8000/api/dcim/devices/1/napalm/?method=get_facts',
+            headers=HEADERS
+        )
+        self.assertTrue(ret)
+        self.assertTrue(ret['get_facts'])
+
 
 class SiteTestCase(unittest.TestCase, GenericTest):
     name = 'sites'
@@ -329,7 +353,8 @@ class InterfaceTestCase(unittest.TestCase, GenericTest):
         self.assertEqual(len(ret), 71)
         mock.assert_called_with(
             'http://localhost:8000/api/dcim/interfaces/?limit=221&offset=50'.format(self.name),
-            headers=HEADERS
+            headers=HEADERS,
+            verify=True
         )
 
 
diff --git a/tests/test_ipam.py b/tests/test_ipam.py
index b752fa2..a3689e9 100644
--- a/tests/test_ipam.py
+++ b/tests/test_ipam.py
@@ -54,7 +54,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_filter(self):
@@ -74,7 +75,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_get(self):
@@ -95,7 +97,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
             if self.ip_obj_fields:
                 for field in self.ip_obj_fields:
@@ -135,7 +138,8 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         ret = pfx.available_ips.list()
         mock.assert_called_with(
             'http://localhost:8000/api/ipam/prefixes/1/available-ips/'.format(self.name),
-            headers=HEADERS
+            headers=HEADERS,
+            verify=True
         )
         self.assertTrue(ret)
         self.assertEqual(len(ret), 3)
@@ -170,6 +174,7 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
             'http://localhost:8000/api/ipam/prefixes/1/available-ips/'.format(self.name),
             headers=POST_HEADERS,
             data=json.dumps(create_parms),
+            verify=True
         )
         self.assertTrue(ret)
         self.assertEqual(ret, expected_result)
@@ -186,7 +191,8 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         ret = pfx.available_prefixes.list()
         mock.assert_called_with(
             'http://localhost:8000/api/ipam/prefixes/1/available-prefixes/'.format(self.name),
-            headers=HEADERS
+            headers=HEADERS,
+            verify=True
         )
         self.assertTrue(ret)
 
@@ -208,6 +214,7 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
             'http://localhost:8000/api/ipam/prefixes/1/available-prefixes/'.format(self.name),
             headers=POST_HEADERS,
             data=json.dumps(create_parms),
+            verify=True
         )
         self.assertTrue(ret)
 
diff --git a/tests/test_query.py b/tests/test_query.py
new file mode 100644
index 0000000..721dbbf
--- /dev/null
+++ b/tests/test_query.py
@@ -0,0 +1,15 @@
+import unittest
+from collections import OrderedDict
+
+from pynetbox.lib.query import url_param_builder
+
+
+class TestQuery(unittest.TestCase):
+
+    def test_url_param_builder(self):
+        test_params = OrderedDict([
+            ('abc', '123'),
+            ('xyz', '321'),
+            ('enc', '#12'),
+        ])
+        self.assertEqual(url_param_builder(test_params), '?abc=123&xyz=321&enc=%2312')
diff --git a/tests/test_tenancy.py b/tests/test_tenancy.py
index 1f2ce2f..35c6dde 100644
--- a/tests/test_tenancy.py
+++ b/tests/test_tenancy.py
@@ -44,7 +44,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_filter(self):
@@ -64,7 +65,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_get(self):
@@ -83,7 +85,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
 
diff --git a/tests/test_virtualization.py b/tests/test_virtualization.py
index 5dc6d81..e80ffd8 100644
--- a/tests/test_virtualization.py
+++ b/tests/test_virtualization.py
@@ -44,7 +44,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_filter(self):
@@ -64,7 +65,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )
 
     def test_get(self):
@@ -83,7 +85,8 @@ class GenericTest(object):
                     self.app,
                     self.name.replace('_', '-')
                 ),
-                headers=HEADERS
+                headers=HEADERS,
+                verify=True
             )