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/

Discussion:

Patch:
diff --git a/SoftLayer/API.py b/SoftLayer/API.py
index 00dbfe74..ea119a4b 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 e2fde6e3..d5b8f584 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 f7d0f1f1..5286d485 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 6484fd7d..db65528b 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 cc0a7f5c..417b9140 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 d7e3871b..0489e597 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 f7f7005e..84ab6665 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 49af7197..261f6f91 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 9159eaaa..4e1cb863 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 d21c8dec..a64efc43 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 0f6bfa78..c7afacd8 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 c68ac7a0..616afdd5 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 438d7702..0ba0a51a 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 54dfc6e3..2b0954c7 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 3c129372..0b46f658 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 0c7c1cad..24519f6b 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 e5c22083..8ccba40a 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 11e35c00..647ddfc2 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 13c5b0fd..82a17d6a 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 aa86fe24..915edad4 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 6fa6599e..fbebc55a 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 00000000..2ee8474c
--- /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)