Repository: softlayer/softlayer-python

Issue:
Emit warning when not using a orderBy filter for iter_call
When a user make an API call that has a resultLimit set but isn't using an orderBy objectFilter of some kind, we should warn the user they might be getting results back no always in the same order. And link them to https://sldn.softlayer.com/article/using-result-limits-softlayer-api/

Hints from issue comments:

Solution patch:
diff --git a/SoftLayer/API.py b/SoftLayer/API.py
index 00dbfe74f..ea119a4b1 100644
--- a/SoftLayer/API.py
+++ b/SoftLayer/API.py
@@ -19,6 +19,7 @@
 from SoftLayer import consts
 from SoftLayer import exceptions
 from SoftLayer import transports
+from SoftLayer import utils
 
 LOGGER = logging.getLogger(__name__)
 API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT
@@ -403,6 +404,7 @@ def iter_call(self, service, method, *args, **kwargs):
         kwargs['iter'] = False
         result_count = 0
         keep_looping = True
+        kwargs['filter'] = utils.fix_filter(kwargs.get('filter'))
 
         while keep_looping:
             # Get the next results
diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py
index e2fde6e30..d5b8f584b 100644
--- a/SoftLayer/CLI/environment.py
+++ b/SoftLayer/CLI/environment.py
@@ -111,6 +111,7 @@ def getpass(self, prompt, default=None):
         # In windows, shift+insert actually inputs the below 2 characters
         # If we detect those 2 characters, need to manually read from the clipbaord instead
         # https://stackoverflow.com/questions/101128/how-do-i-read-text-from-the-clipboard
+        # LINUX NOTICE: `apt-get install python3-tk` required to install tk
         if password == 'àR':
             # tkinter is a built in python gui, but it has clipboard reading functions.
             # pylint: disable=import-outside-toplevel
diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py
index f7d0f1f11..5286d485f 100644
--- a/SoftLayer/managers/block.py
+++ b/SoftLayer/managers/block.py
@@ -53,23 +53,21 @@ def list_block_volumes(self, datacenter=None, username=None, storage_type=None,
         _filter = utils.NestedDict(kwargs.get('filter') or {})
 
         _filter['iscsiNetworkStorage']['serviceResource']['type']['type'] = utils.query_filter('!~ ISCSI')
+        _filter['iscsiNetworkStorage']['id'] = utils.query_filter_orderby()
 
-        _filter['iscsiNetworkStorage']['storageType']['keyName'] = (
-            utils.query_filter('*BLOCK_STORAGE*'))
+        _filter['iscsiNetworkStorage']['storageType']['keyName'] = utils.query_filter('*BLOCK_STORAGE*')
         if storage_type:
             _filter['iscsiNetworkStorage']['storageType']['keyName'] = (
                 utils.query_filter('%s_BLOCK_STORAGE*' % storage_type.upper()))
 
         if datacenter:
-            _filter['iscsiNetworkStorage']['serviceResource']['datacenter'][
-                'name'] = utils.query_filter(datacenter)
+            _filter['iscsiNetworkStorage']['serviceResource']['datacenter']['name'] = utils.query_filter(datacenter)
 
         if username:
             _filter['iscsiNetworkStorage']['username'] = utils.query_filter(username)
 
         if order:
-            _filter['iscsiNetworkStorage']['billingItem']['orderItem'][
-                'order']['id'] = utils.query_filter(order)
+            _filter['iscsiNetworkStorage']['billingItem']['orderItem']['order']['id'] = utils.query_filter(order)
 
         kwargs['filter'] = _filter.to_dict()
         return self.client.call('Account', 'getIscsiNetworkStorage', iter=True, **kwargs)
diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py
index 6484fd7d9..db65528bf 100644
--- a/SoftLayer/managers/dns.py
+++ b/SoftLayer/managers/dns.py
@@ -196,6 +196,7 @@ def get_records(self, zone_id, ttl=None, data=None, host=None, record_type=None)
         :returns: A list of dictionaries representing the matching records within the specified zone.
         """
         _filter = utils.NestedDict()
+        _filter['resourceRecords']['id'] = utils.query_filter_orderby()
 
         if ttl:
             _filter['resourceRecords']['ttl'] = utils.query_filter(ttl)
diff --git a/SoftLayer/managers/event_log.py b/SoftLayer/managers/event_log.py
index cc0a7f5cd..417b91409 100644
--- a/SoftLayer/managers/event_log.py
+++ b/SoftLayer/managers/event_log.py
@@ -65,11 +65,8 @@ def build_filter(date_min=None, date_max=None, obj_event=None, obj_id=None, obj_
 
         :returns: dict: The generated query filter
         """
-
-        if not any([date_min, date_max, obj_event, obj_id, obj_type]):
-            return {}
-
         request_filter = {}
+        request_filter['traceId'] = utils.query_filter_orderby()
 
         if date_min and date_max:
             request_filter['eventCreateDate'] = utils.event_log_filter_between_date(date_min, date_max, utc_offset)
diff --git a/SoftLayer/managers/file.py b/SoftLayer/managers/file.py
index d7e3871b9..0489e597c 100644
--- a/SoftLayer/managers/file.py
+++ b/SoftLayer/managers/file.py
@@ -47,7 +47,7 @@ def list_file_volumes(self, datacenter=None, username=None, storage_type=None, o
             kwargs['mask'] = ','.join(items)
 
         _filter = utils.NestedDict(kwargs.get('filter') or {})
-
+        _filter['nasNetworkStorage']['id'] = utils.query_filter_orderby()
         _filter['nasNetworkStorage']['serviceResource']['type']['type'] = utils.query_filter('!~ NAS')
 
         _filter['nasNetworkStorage']['storageType']['keyName'] = (
diff --git a/SoftLayer/managers/image.py b/SoftLayer/managers/image.py
index f7f7005eb..84ab6665e 100644
--- a/SoftLayer/managers/image.py
+++ b/SoftLayer/managers/image.py
@@ -57,13 +57,12 @@ def list_private_images(self, guid=None, name=None, limit=100, **kwargs):
             kwargs['mask'] = IMAGE_MASK
 
         _filter = utils.NestedDict(kwargs.get('filter') or {})
+        _filter['privateBlockDeviceTemplateGroups']['id'] = utils.query_filter_orderby()
         if name:
-            _filter['privateBlockDeviceTemplateGroups']['name'] = (
-                utils.query_filter(name))
+            _filter['privateBlockDeviceTemplateGroups']['name'] = utils.query_filter(name)
 
         if guid:
-            _filter['privateBlockDeviceTemplateGroups']['globalIdentifier'] = (
-                utils.query_filter(guid))
+            _filter['privateBlockDeviceTemplateGroups']['globalIdentifier'] = utils.query_filter(guid)
 
         kwargs['filter'] = _filter.to_dict()
 
@@ -81,6 +80,7 @@ def list_public_images(self, guid=None, name=None, limit=100, **kwargs):
             kwargs['mask'] = IMAGE_MASK
 
         _filter = utils.NestedDict(kwargs.get('filter') or {})
+        _filter['id'] = utils.query_filter_orderby()
         if name:
             _filter['name'] = utils.query_filter(name)
 
diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py
index 49af7197f..261f6f91b 100644
--- a/SoftLayer/managers/network.py
+++ b/SoftLayer/managers/network.py
@@ -496,6 +496,7 @@ def list_subnets(self, identifier=None, datacenter=None, version=0,
             kwargs['mask'] = DEFAULT_SUBNET_MASK
 
         _filter = utils.NestedDict(kwargs.get('filter') or {})
+        _filter['subnets']['id'] = utils.query_filter_orderby()
 
         if identifier:
             _filter['subnets']['networkIdentifier'] = (
diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py
index 9159eaaa6..4e1cb8636 100644
--- a/SoftLayer/utils.py
+++ b/SoftLayer/utils.py
@@ -6,6 +6,7 @@
 """
 
 import collections
+import copy
 import datetime
 from json import JSONDecoder
 import re
@@ -41,6 +42,32 @@ def lookup(dic, key, *keys):
     return dic.get(key)
 
 
+def has_key_value(d: dict, key: str = "operation", value: str = "orderBy") -> bool:
+    """Scan through a dictionary looking for an orderBy clause, but can be used for any key/value combo"""
+    if d.get(key) and d.get(key) == value:
+        return True
+    for x in d.values():
+        if isinstance(x, dict):
+            if has_key_value(x, key, value):
+                return True
+    return False
+
+
+def fix_filter(sl_filter: dict = None) -> dict:
+    """Forces an object filter to have an orderBy  clause if it doesn't have one already"""
+
+    if sl_filter is None:
+        sl_filter = {}
+
+    # Make a copy to prevent sl_filter from being modified by this function
+    this_filter = copy.copy(sl_filter)
+    if not has_key_value(this_filter, "operation", "orderBy"):
+        # Check to see if 'id' is already a filter, if so just skip
+        if not this_filter.get('id', False):
+            this_filter['id'] = query_filter_orderby()
+    return this_filter
+
+
 class NestedDict(dict):
     """This helps with accessing a heavily nested dictionary.
 


Test patch:
diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py
index d21c8dece..a64efc432 100644
--- a/tests/CLI/modules/block_tests.py
+++ b/tests/CLI/modules/block_tests.py
@@ -125,7 +125,8 @@ def test_volume_detail_name_identifier(self):
                 'storageType': {
                     'keyName': {'operation': '*= BLOCK_STORAGE'}
                 },
-                'username': {'operation': '_= SL-12345'}
+                'username': {'operation': '_= SL-12345'},
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
             }
         }
 
diff --git a/tests/CLI/modules/event_log_tests.py b/tests/CLI/modules/event_log_tests.py
index 0f6bfa789..c7afacd8b 100644
--- a/tests/CLI/modules/event_log_tests.py
+++ b/tests/CLI/modules/event_log_tests.py
@@ -31,10 +31,9 @@ def test_get_event_log_empty(self):
         mock.return_value = None
 
         result = self.run_command(['event-log', 'get'])
-        expected = 'Event, Object, Type, Date, Username\n' \
-                   'No logs available for filter {}.\n'
+
         self.assert_no_fail(result)
-        self.assertEqual(expected, result.output)
+        self.assertIn("No logs available for filter ", result.output)
 
     def test_get_event_log_over_limit(self):
         result = self.run_command(['event-log', 'get', '-l 1'])
diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py
index c68ac7a08..616afdd5c 100644
--- a/tests/CLI/modules/file_tests.py
+++ b/tests/CLI/modules/file_tests.py
@@ -210,14 +210,13 @@ def test_volume_detail_name_identifier(self):
         expected_filter = {
             'nasNetworkStorage': {
                 'serviceResource': {
-                    'type': {
-                        'type': {'operation': '!~ NAS'}
-                    }
+                    'type': {'type': {'operation': '!~ NAS'}}
                 },
-                'storageType': {
-                    'keyName': {'operation': '*= FILE_STORAGE'}
-                },
-                'username': {'operation': '_= SL-12345'}}}
+                'storageType': {'keyName': {'operation': '*= FILE_STORAGE'}},
+                'username': {'operation': '_= SL-12345'},
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+            }
+        }
 
         self.assert_called_with('SoftLayer_Account', 'getNasNetworkStorage', filter=expected_filter)
         self.assert_called_with('SoftLayer_Network_Storage', 'getObject', identifier=1)
diff --git a/tests/api_tests.py b/tests/api_tests.py
index 438d77020..0ba0a51ad 100644
--- a/tests/api_tests.py
+++ b/tests/api_tests.py
@@ -169,8 +169,8 @@ def test_iter_call(self, _call):
 
         self.assertEqual(list(range(125)), result)
         _call.assert_has_calls([
-            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=0),
-            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=100),
+            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=0, filter=mock.ANY),
+            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=100, filter=mock.ANY),
         ])
         _call.reset_mock()
 
@@ -183,9 +183,9 @@ def test_iter_call(self, _call):
         result = list(self.client.iter_call('SERVICE', 'METHOD', iter=True))
         self.assertEqual(list(range(200)), result)
         _call.assert_has_calls([
-            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=0),
-            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=100),
-            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=200),
+            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=0, filter=mock.ANY),
+            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=100, filter=mock.ANY),
+            mock.call('SERVICE', 'METHOD', limit=100, iter=False, offset=200, filter=mock.ANY),
         ])
         _call.reset_mock()
 
@@ -194,12 +194,11 @@ def test_iter_call(self, _call):
             transports.SoftLayerListResult(range(0, 25), 30),
             transports.SoftLayerListResult(range(25, 30), 30)
         ]
-        result = list(self.client.iter_call(
-            'SERVICE', 'METHOD', iter=True, limit=25))
+        result = list(self.client.iter_call('SERVICE', 'METHOD', iter=True, limit=25))
         self.assertEqual(list(range(30)), result)
         _call.assert_has_calls([
-            mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=0),
-            mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=25),
+            mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=0, filter=mock.ANY),
+            mock.call('SERVICE', 'METHOD', iter=False, limit=25, offset=25, filter=mock.ANY),
         ])
         _call.reset_mock()
 
@@ -208,7 +207,7 @@ def test_iter_call(self, _call):
         result = list(self.client.iter_call('SERVICE', 'METHOD', iter=True))
         self.assertEqual(["test"], result)
         _call.assert_has_calls([
-            mock.call('SERVICE', 'METHOD', iter=False, limit=100, offset=0),
+            mock.call('SERVICE', 'METHOD', iter=False, limit=100, offset=0, filter=mock.ANY),
         ])
         _call.reset_mock()
 
@@ -216,23 +215,19 @@ def test_iter_call(self, _call):
             transports.SoftLayerListResult(range(0, 25), 30),
             transports.SoftLayerListResult(range(25, 30), 30)
         ]
-        result = list(self.client.iter_call('SERVICE', 'METHOD', 'ARG',
-                                            iter=True,
-                                            limit=25,
-                                            offset=12))
+        result = list(
+            self.client.iter_call('SERVICE', 'METHOD', 'ARG', iter=True, limit=25, offset=12)
+        )
         self.assertEqual(list(range(30)), result)
         _call.assert_has_calls([
-            mock.call('SERVICE', 'METHOD', 'ARG',
-                      iter=False, limit=25, offset=12),
-            mock.call('SERVICE', 'METHOD', 'ARG',
-                      iter=False, limit=25, offset=37),
+            mock.call('SERVICE', 'METHOD', 'ARG', iter=False, limit=25, offset=12, filter=mock.ANY),
+            mock.call('SERVICE', 'METHOD', 'ARG', iter=False, limit=25, offset=37, filter=mock.ANY),
         ])
 
         # Chunk size of 0 is invalid
         self.assertRaises(
             AttributeError,
-            lambda: list(self.client.iter_call('SERVICE', 'METHOD',
-                                               iter=True, limit=0)))
+            lambda: list(self.client.iter_call('SERVICE', 'METHOD', iter=True, limit=0, filter=mock.ANY)))
 
     def test_call_invalid_arguments(self):
         self.assertRaises(
diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py
index 54dfc6e36..2b0954c7d 100644
--- a/tests/managers/block_tests.py
+++ b/tests/managers/block_tests.py
@@ -125,8 +125,7 @@ def test_get_block_volume_details(self):
     def test_list_block_volumes(self):
         result = self.block.list_block_volumes()
 
-        self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage,
-                         result)
+        self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, result)
 
         expected_filter = {
             'iscsiNetworkStorage': {
@@ -134,10 +133,9 @@ def test_list_block_volumes(self):
                     'keyName': {'operation': '*= BLOCK_STORAGE'}
                 },
                 'serviceResource': {
-                    'type': {
-                        'type': {'operation': '!~ ISCSI'}
-                    }
-                }
+                    'type': {'type': {'operation': '!~ ISCSI'}}
+                },
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
             }
         }
 
@@ -161,8 +159,7 @@ def test_list_block_volumes(self):
     def test_list_block_volumes_additional_filter_order(self):
         result = self.block.list_block_volumes(order=1234567)
 
-        self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage,
-                         result)
+        self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, result)
 
         expected_filter = {
             'iscsiNetworkStorage': {
@@ -170,14 +167,12 @@ def test_list_block_volumes_additional_filter_order(self):
                     'keyName': {'operation': '*= BLOCK_STORAGE'}
                 },
                 'serviceResource': {
-                    'type': {
-                        'type': {'operation': '!~ ISCSI'}
-                    }
+                    'type': {'type': {'operation': '!~ ISCSI'}}
                 },
                 'billingItem': {
-                    'orderItem': {
-                        'order': {
-                            'id': {'operation': 1234567}}}}
+                    'orderItem': {'order': {'id': {'operation': 1234567}}}
+                },
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
             }
         }
 
@@ -199,27 +194,21 @@ def test_list_block_volumes_additional_filter_order(self):
         )
 
     def test_list_block_volumes_with_additional_filters(self):
-        result = self.block.list_block_volumes(datacenter="dal09",
-                                               storage_type="Endurance",
-                                               username="username")
+        result = self.block.list_block_volumes(datacenter="dal09", storage_type="Endurance", username="username")
 
-        self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage,
-                         result)
+        self.assertEqual(SoftLayer_Account.getIscsiNetworkStorage, result)
 
         expected_filter = {
             'iscsiNetworkStorage': {
                 'storageType': {
                     'keyName': {'operation': '^= ENDURANCE_BLOCK_STORAGE'}
                 },
-                'username': {'operation': u'_= username'},
+                'username': {'operation': '_= username'},
                 'serviceResource': {
-                    'datacenter': {
-                        'name': {'operation': u'_= dal09'}
-                    },
-                    'type': {
-                        'type': {'operation': '!~ ISCSI'}
-                    }
-                }
+                    'datacenter': {'name': {'operation': u'_= dal09'}},
+                    'type': {'type': {'operation': '!~ ISCSI'}}
+                },
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
             }
         }
 
diff --git a/tests/managers/dedicated_host_tests.py b/tests/managers/dedicated_host_tests.py
index 3c1293726..0b46f6585 100644
--- a/tests/managers/dedicated_host_tests.py
+++ b/tests/managers/dedicated_host_tests.py
@@ -714,7 +714,8 @@ def test_list_guests_with_filters(self):
                 'networkComponents': {'maxSpeed': {'operation': 100}},
                 'primaryIpAddress': {'operation': '_= 1.2.3.4'},
                 'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'}
-            }
+            },
+            'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
         self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'getGuests',
                                 identifier=12345, filter=_filter)
diff --git a/tests/managers/dns_tests.py b/tests/managers/dns_tests.py
index 0c7c1cade..24519f6b8 100644
--- a/tests/managers/dns_tests.py
+++ b/tests/managers/dns_tests.py
@@ -167,13 +167,16 @@ def test_get_records(self):
         mock.return_value = [records[0]]
         self.dns_client.get_records(12345, record_type='a', host='hostname', data='a', ttl='86400')
 
-        _filter = {'resourceRecords': {'type': {'operation': '_= a'},
-                                       'host': {'operation': '_= hostname'},
-                                       'data': {'operation': '_= a'},
-                                       'ttl': {'operation': 86400}}}
-        self.assert_called_with('SoftLayer_Dns_Domain', 'getResourceRecords',
-                                identifier=12345,
-                                filter=_filter)
+        _filter = {
+            'resourceRecords': {
+                'type': {'operation': '_= a'},
+                'host': {'operation': '_= hostname'},
+                'data': {'operation': '_= a'},
+                'ttl': {'operation': 86400},
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+            }
+        }
+        self.assert_called_with('SoftLayer_Dns_Domain', 'getResourceRecords', identifier=12345, filter=_filter)
 
     def test_get_record(self):
         record_id = 1234
diff --git a/tests/managers/event_log_tests.py b/tests/managers/event_log_tests.py
index e5c220835..8ccba40a2 100644
--- a/tests/managers/event_log_tests.py
+++ b/tests/managers/event_log_tests.py
@@ -39,8 +39,8 @@ def test_get_event_log_types(self):
 
     def test_build_filter_no_args(self):
         result = self.event_log.build_filter(None, None, None, None, None, None)
-
-        self.assertEqual(result, {})
+        expected = {'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}}
+        self.assertDictEqual(result, expected)
 
     def test_build_filter_min_date(self):
         expected = {
@@ -54,7 +54,8 @@ def test_build_filter_min_date(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter('10/30/2017', None, None, None, None, None)
@@ -73,7 +74,8 @@ def test_build_filter_max_date(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter(None, '10/31/2017', None, None, None, None)
@@ -98,7 +100,8 @@ def test_build_filter_min_max_date(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, None)
@@ -117,7 +120,8 @@ def test_build_filter_min_date_pos_utc(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter('10/30/2017', None, None, None, None, '+0500')
@@ -136,7 +140,8 @@ def test_build_filter_max_date_pos_utc(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter(None, '10/31/2017', None, None, None, '+0500')
@@ -161,7 +166,8 @@ def test_build_filter_min_max_date_pos_utc(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, '+0500')
@@ -180,7 +186,8 @@ def test_build_filter_min_date_neg_utc(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter('10/30/2017', None, None, None, None, '-0300')
@@ -199,7 +206,8 @@ def test_build_filter_max_date_neg_utc(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter(None, '10/31/2017', None, None, None, '-0300')
@@ -224,7 +232,8 @@ def test_build_filter_min_max_date_neg_utc(self):
                         ]
                     }
                 ]
-            }
+            },
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         result = self.event_log.build_filter('10/30/2017', '10/31/2017', None, None, None, '-0300')
@@ -232,21 +241,30 @@ def test_build_filter_min_max_date_neg_utc(self):
         self.assertEqual(expected, result)
 
     def test_build_filter_name(self):
-        expected = {'eventName': {'operation': 'Add Security Group'}}
+        expected = {
+            'eventName': {'operation': 'Add Security Group'},
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+        }
 
         result = self.event_log.build_filter(None, None, 'Add Security Group', None, None, None)
 
         self.assertEqual(expected, result)
 
     def test_build_filter_id(self):
-        expected = {'objectId': {'operation': 1}}
+        expected = {
+            'objectId': {'operation': 1},
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+        }
 
         result = self.event_log.build_filter(None, None, None, 1, None, None)
 
         self.assertEqual(expected, result)
 
     def test_build_filter_type(self):
-        expected = {'objectName': {'operation': 'CCI'}}
+        expected = {
+            'objectName': {'operation': 'CCI'},
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+        }
 
         result = self.event_log.build_filter(None, None, None, None, 'CCI', None)
 
diff --git a/tests/managers/file_tests.py b/tests/managers/file_tests.py
index 11e35c001..647ddfc2c 100644
--- a/tests/managers/file_tests.py
+++ b/tests/managers/file_tests.py
@@ -356,7 +356,8 @@ def test_list_file_volumes(self):
                     'type': {
                         'type': {'operation': '!~ NAS'}
                     }
-                }
+                },
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
             }
         }
 
@@ -389,14 +390,12 @@ def test_list_file_volumes_additional_filter_order(self):
                     'keyName': {'operation': '*= FILE_STORAGE'}
                 },
                 'serviceResource': {
-                    'type': {
-                        'type': {'operation': '!~ NAS'}
-                    }
+                    'type': {'type': {'operation': '!~ NAS'}}
                 },
                 'billingItem': {
-                    'orderItem': {
-                        'order': {
-                            'id': {'operation': 1234567}}}}
+                    'orderItem': {'order': {'id': {'operation': 1234567}}}
+                },
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
             }
         }
 
@@ -430,15 +429,12 @@ def test_list_file_volumes_with_additional_filters(self):
                 'storageType': {
                     'keyName': {'operation': '^= ENDURANCE_FILE_STORAGE'}
                 },
-                'username': {'operation': u'_= username'},
+                'username': {'operation': '_= username'},
                 'serviceResource': {
-                    'datacenter': {
-                        'name': {'operation': u'_= dal09'}
-                    },
-                    'type': {
-                        'type': {'operation': '!~ NAS'}
-                    }
-                }
+                    'datacenter': {'name': {'operation': '_= dal09'}},
+                    'type': {'type': {'operation': '!~ NAS'}}
+                },
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
             }
         }
 
diff --git a/tests/managers/image_tests.py b/tests/managers/image_tests.py
index 13c5b0fdf..82a17d6ad 100644
--- a/tests/managers/image_tests.py
+++ b/tests/managers/image_tests.py
@@ -46,7 +46,9 @@ def test_list_private_images_with_filters(self):
             'privateBlockDeviceTemplateGroups': {
                 'globalIdentifier': {
                     'operation': '_= 0FA9ECBD-CF7E-4A1F-1E36F8D27C2B'},
-                'name': {'operation': '_= name'}}
+                'name': {'operation': '_= name'},
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+            }
         }
         self.assertEqual(len(results), 2)
         self.assert_called_with('SoftLayer_Account', 'getPrivateBlockDeviceTemplateGroups', filter=_filter)
@@ -64,7 +66,8 @@ def test_list_public_images_with_filters(self):
         _filter = {
             'globalIdentifier': {
                 'operation': '_= 0FA9ECBD-CF7E-4A1F-1E36F8D27C2B'},
-            'name': {'operation': '_= name'}
+            'name': {'operation': '_= name'},
+            'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
         }
 
         self.assert_called_with(IMAGE_SERVICE, 'getPublicImages', filter=_filter)
diff --git a/tests/managers/network_tests.py b/tests/managers/network_tests.py
index aa86fe245..915edad4d 100644
--- a/tests/managers/network_tests.py
+++ b/tests/managers/network_tests.py
@@ -327,6 +327,7 @@ def test_list_subnets_default(self):
                 'version': {'operation': 4},
                 'subnetType': {'operation': '_= PRIMARY'},
                 'networkVlan': {'networkSpace': {'operation': '_= PUBLIC'}},
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
             }
         }
 
@@ -602,7 +603,10 @@ def test_get_security_group_event_logs(self):
         result = self.network._get_security_group_event_logs()
         # Event log now returns a generator, so you have to get a result for it to make an API call
         log = result.__next__()
-        _filter = {'objectName': {'operation': 'Security Group'}}
+        _filter = {
+            'objectName': {'operation': 'Security Group'},
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+        }
         self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects', filter=_filter)
         self.assertEqual(100, log['accountId'])
 
@@ -611,7 +615,10 @@ def test_get_cci_event_logs(self):
         result = self.network._get_cci_event_logs()
         # Event log now returns a generator, so you have to get a result for it to make an API call
         log = result.__next__()
-        _filter = {'objectName': {'operation': 'CCI'}}
+        _filter = {
+            'objectName': {'operation': 'CCI'},
+            'traceId': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+        }
         self.assert_called_with('SoftLayer_Event_Log', 'getAllObjects', filter=_filter)
         self.assertEqual(100, log['accountId'])
 
diff --git a/tests/managers/vs/vs_capacity_tests.py b/tests/managers/vs/vs_capacity_tests.py
index 6fa6599e8..fbebc55a5 100644
--- a/tests/managers/vs/vs_capacity_tests.py
+++ b/tests/managers/vs/vs_capacity_tests.py
@@ -49,8 +49,13 @@ def test_get_available_routers(self):
     def test_get_available_routers_search(self):
 
         result = self.manager.get_available_routers('wdc07')
-        package_filter = {'keyName': {'operation': 'RESERVED_CAPACITY'}}
-        pod_filter = {'datacenterName': {'operation': 'wdc07'}}
+        package_filter = {
+            'keyName': {'operation': 'RESERVED_CAPACITY'}
+        }
+        pod_filter = {
+            'datacenterName': {'operation': 'wdc07'},
+            'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+        }
         self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects', mask=mock.ANY, filter=package_filter)
         self.assert_called_with('SoftLayer_Product_Package', 'getRegions', mask=mock.ANY)
         self.assert_called_with('SoftLayer_Network_Pod', 'getAllObjects', filter=pod_filter)
diff --git a/tests/utils_tests.py b/tests/utils_tests.py
new file mode 100644
index 000000000..2ee8474ca
--- /dev/null
+++ b/tests/utils_tests.py
@@ -0,0 +1,84 @@
+"""
+    SoftLayer.tests.utils_tests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    Tests shared code
+
+    :license: MIT, see LICENSE for more details.
+"""
+from SoftLayer import testing
+from SoftLayer import utils
+
+
+TEST_FILTER = {
+    'virtualGuests': {
+        'provisionDate': {
+            'operation': 'orderBy',
+            'options': [
+                {'name': 'sort', 'value': ['DESC']},
+                {'name': 'sortOrder', 'value': [1]}
+            ]
+        },
+        'maxMemory': {
+            'operation': 'orderBy',
+            'options': [
+                {'name': 'sort', 'value': ['ASC']},
+                {'name': 'sortOrder', 'value': [0]}
+            ]
+        },
+    },
+    'hardware': {
+        'sparePoolBillingItem': {
+            'id': {'operation': 'not null'}
+        }
+    },
+    'someProperty': {
+        'provisionDate': {
+            'operation': '> sysdate - 30'
+        }
+    }
+}
+
+
+class TestUtils(testing.TestCase):
+
+    def test_find_key_simple(self):
+        """Simple test case"""
+        test_dict = {"key1": "value1", "nested": {"key2": "value2", "key3": "value4"}}
+        result = utils.has_key_value(test_dict, "key2", "value2")
+        self.assertIsNotNone(result)
+        self.assertTrue(result)
+
+    def test_find_object_filter(self):
+        """Find first orderBy operation in a real-ish object filter"""
+
+        result = utils.has_key_value(TEST_FILTER)
+        self.assertIsNotNone(result)
+        self.assertTrue(result)
+
+    def test_not_found(self):
+        """Nothing to be found"""
+        test_dict = {"key1": "value1", "nested": {"key2": "value2", "key3": "value4"}}
+        result = utils.has_key_value(test_dict, "key23", "value2")
+        self.assertFalse(result)
+
+    def test_fix_filter(self):
+        original_filter = {}
+        fixed_filter = utils.fix_filter(original_filter)
+        self.assertIsNotNone(fixed_filter)
+        self.assertEqual(fixed_filter.get('id'), utils.query_filter_orderby())
+        # testing to make sure original doesn't get changed by the function call
+        self.assertIsNone(original_filter.get('id'))
+
+    def test_billing_filter(self):
+        billing_filter = {
+            'allTopLevelBillingItems': {
+                'cancellationDate': {'operation': 'is null'},
+                'id': {'operation': 'orderBy', 'options': [{'name': 'sort', 'value': ['ASC']}]}
+            }
+        }
+
+        fixed_filter = utils.fix_filter(billing_filter)
+        # Make sure we didn't add any more items
+        self.assertEqual(len(fixed_filter), 1)
+        self.assertEqual(len(fixed_filter.get('allTopLevelBillingItems')), 2)
+        self.assertDictEqual(fixed_filter, billing_filter)