diff --git a/README.md b/README.md index 15e0a7ee05a4bba34a8f3294078ccb20107c77f4..8a13d23b6137f8d0b0d1361d59a5145d18d4a6c4 100644 --- a/README.md +++ b/README.md @@ -212,3 +212,9 @@ def your_callback_func(sender, **kwargs): ``` For more information on signals refer to the django docs. They are great. Really. + +### Testing + +`postgrestutils` has a bunch of unittests because manually testing it has become quite time-consuming. +The tests aim to ensure functional correctness as well as some performance related concerns i.e. caching and lazyness. +With `requests-mock` installed, running is as easy as `python -m unittest`. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_postgrestclient.py b/tests/test_postgrestclient.py new file mode 100644 index 0000000000000000000000000000000000000000..1cb39c891ee0a1ed738ef9a48af9b38b25952be0 --- /dev/null +++ b/tests/test_postgrestclient.py @@ -0,0 +1,317 @@ +import datetime +from unittest import TestCase + +from requests_mock import Mocker + +from postgrestutils.client import ( + Count, MultipleObjectsReturned, ObjectDoesNotExist, +) +from postgrestutils.client.postgrestclient import LazyPostgrestJsonResult + +TOKEN = 'JWT_token' +DEFAULT_HEADERS = { + 'Authorization': 'Bearer {}'.format(TOKEN), + 'Accept': 'application/json' +} +SUPERHERO_TEST_DATA = [ + { + 'id': 68, + 'name': 'Batman', + 'gender': 'Male', + 'eye_color': 'blue', + 'race': 'Human', + 'hair_color': 'black', + 'height': 188, + 'publisher': 'DC Comics', + 'skin_color': None, + 'alignment': 'good', + 'weight': 95 + }, { + 'id': 212, + 'name': 'Deadpool', + 'gender': 'Male', + 'eye_color': 'brown', + 'race': 'Mutant', + 'hair_color': 'No Hair', + 'height': 188, + 'publisher': 'Marvel Comics', + 'skin_color': None, + 'alignment': 'neutral', + 'weight': 95 + }, { + 'id': 345, + 'name': 'Iron Man', + 'gender': 'Male', + 'eye_color': 'blue', + 'race': 'Human', + 'hair_color': 'Black', + 'height': 198, + 'publisher': 'Marvel Comics', + 'skin_color': None, + 'alignment': 'good', + 'weight': 191 + }, { + 'id': 369, + 'name': 'Joker', + 'gender': 'Male', + 'eye_color': 'green', + 'race': 'Human', + 'hair_color': 'Green', + 'height': 196, + 'publisher': 'DC Comics', + 'skin_color': 'white', + 'alignment': 'bad', + 'weight': 86 + }, { + 'id': 423, + 'name': 'Magneto', + 'gender': 'Male', + 'eye_color': 'grey', + 'race': 'Mutant', + 'hair_color': 'White', + 'height': 188, + 'publisher': 'Marvel Comics', + 'skin_color': None, + 'alignment': 'bad', + 'weight': 86 + } +] + + +@Mocker() +class TestPgrestClientGet(TestCase): + def setUp(self): + super().setUp() + from postgrestutils.client import pgrest_client + pgrest_client.configure(TOKEN, base_uri='http://example.com/') + + self.pgrest_client = pgrest_client + self.data = SUPERHERO_TEST_DATA[0] + + def test_single_object_returned(self, mock): + mock.register_uri( + 'GET', + 'http://example.com/superhero?id=eq.1000000000', + request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}}, + status_code=200, + reason='OK', + json=self.data + ) + params = {'id': 'eq.1000000000'} + res = self.pgrest_client.get('superhero', params=params) + + self.assertDictEqual(res, self.data) + self.assertTrue(mock.called_once) + + def test_object_does_not_exist(self, mock): + mock.register_uri( + 'GET', + 'http://example.com/superhero?id=eq.1337', + request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}}, + status_code=406, + reason='Not Acceptable', + text="""{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":""" + """"JSON object requested, multiple (or no) rows returned"}""" + ) + params = {'id': 'eq.1337'} + + with self.assertRaises(ObjectDoesNotExist): + self.pgrest_client.get('superhero', params=params) + self.assertTrue(mock.called_once) + + def test_multiple_objects_returned(self, mock): + mock.register_uri( + 'GET', + 'http://example.com/superhero', + request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}}, + status_code=406, + reason='Not Acceptable', + text="""{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":""" + """"JSON object requested, multiple (or no) rows returned"}""" + ) + + with self.assertRaises(MultipleObjectsReturned): + self.pgrest_client.get('superhero') + self.assertTrue(mock.called_once) + + def test_datetime_parser(self, mock): + expected = { + 'id': 1337, + 'random': datetime.datetime(2020, 5, 20, 8, 35, 6, 659425, tzinfo=datetime.timezone.utc) + } + mock.register_uri( + 'GET', + 'http://example.com/random_datetime', + request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}}, + status_code=200, + reason='OK', + json={'id': 1337, 'random': "2020-05-20T08:35:06.659425+00:00"} + ) + params = {'id': 'eq.1337'} + res = self.pgrest_client.get('random_datetime', params=params) + + self.assertDictEqual(res, expected) + self.assertTrue(mock.called_once) + + def test_without_datetime_parser(self, mock): + test_json = {'id': 1337, 'random': "2020-05-20T08:35:06.659425+00:00"} + mock.register_uri( + 'GET', + 'http://example.com/random_datetime', + request_headers={**DEFAULT_HEADERS, **{'Accept': 'application/vnd.pgrst.object+json'}}, + status_code=200, + reason='OK', + json=test_json + ) + params = {'select': 'id,random', 'id': 'eq.1337'} + res = self.pgrest_client.get('random_datetime', params=params, parse_dt=False) + + self.assertDictEqual(res, test_json) + self.assertTrue(mock.called_once) + + +@Mocker() +class TestPgrestClientFilterStrategyNone(TestCase): + def setUp(self): + super().setUp() + from postgrestutils.client import pgrest_client + pgrest_client.configure(TOKEN, base_uri='http://example.com/') + + self.pgrest_client = pgrest_client + self.data = SUPERHERO_TEST_DATA + + def test_fetch_all_first(self, mock): + mock.register_uri( + 'GET', + 'http://example.com/superhero', + request_headers=DEFAULT_HEADERS, + status_code=200, + reason='OK', + json=self.data + ) + res = self.pgrest_client.filter('superhero') + + self.assertIsInstance(res, LazyPostgrestJsonResult) # should return lazy object + self.assertFalse(mock.called) # no request should have been made yet + + self.assertListEqual(list(res), self.data) # fetch data + self.assertTrue(mock.called_once) # should have been called once + self.assertListEqual(res._result_cache, self.data) # fetched data should be cached + self.assertEqual(res._len_cache, len(self.data)) # len of fetched data should be cached + self.assertListEqual(list(res), self.data) # should utilize cache + self.assertListEqual(res[2:], self.data[2:]) # should utilize cache + self.assertDictEqual(res[0], self.data[0]) # should utilize cache + self.assertTrue(mock.called_once) # should not have been called again + + def test_fetch_len_first(self, mock): + mock.register_uri( + 'GET', + 'http://example.com/superhero', + request_headers=DEFAULT_HEADERS, + status_code=200, + reason='OK', + json=self.data + ) + res = self.pgrest_client.filter('superhero') + + self.assertIsInstance(res, LazyPostgrestJsonResult) # should return lazy object + self.assertFalse(mock.called) # no request should have been made yet + + self.assertEqual(len(res), len(self.data)) # should fetch len + self.assertTrue(mock.called_once) # should have been called once + self.assertEqual(res._len_cache, len(self.data)) # len of fetched data should be cached + self.assertListEqual(res._result_cache, self.data) # results should be cached (counting strategy none) + self.assertListEqual(res[2:], self.data[2:]) # should utilize cache + self.assertDictEqual(res[0], self.data[0]) # should utilize cache + self.assertListEqual(list(res), self.data) # should utilize cache + self.assertTrue(mock.called_once) # should not have been called again + + +@Mocker() +class TestPgrestClientFilterStrategyExact(TestCase): + def setUp(self): + super().setUp() + from postgrestutils.client import pgrest_client + pgrest_client.configure(TOKEN, base_uri='http://example.com/') + + self.pgrest_client = pgrest_client + self.data = SUPERHERO_TEST_DATA + + def test_fetch_all_first(self, mock): + # in order to fetch all + mock.register_uri( + 'GET', + 'http://example.com/superhero', + request_headers=DEFAULT_HEADERS, + status_code=200, + reason='OK', + json=self.data + ) + res = self.pgrest_client.filter('superhero', count=Count.EXACT) + + self.assertIsInstance(res, LazyPostgrestJsonResult) # should return lazy object + self.assertFalse(mock.called) # no request should have been made yet + + self.assertListEqual(list(res), self.data) # fetch data + self.assertTrue(mock.called_once) # should have been called once + self.assertListEqual(res._result_cache, self.data) # fetched data should be cached + self.assertEqual(res._len_cache, len(self.data)) # len of fetched data should also be cached + self.assertListEqual(list(res), self.data) # should utilize cache + self.assertListEqual(res[2:], self.data[2:]) # should utilize cache + self.assertDictEqual(res[0], self.data[0]) # should utilize cache + self.assertTrue(mock.called_once) # should not have been called again + + def test_fetch_len_first(self, mock): + # in order to fetch all + mock.register_uri( + 'GET', + 'http://example.com/superhero', + request_headers=DEFAULT_HEADERS, + status_code=200, + reason='OK', + json=self.data + ) + # in order to fetch first + mock.register_uri( + 'GET', + 'http://example.com/superhero', + request_headers={**DEFAULT_HEADERS, **{'Range-Unit': 'items', 'Range': '0-0'}}, + status_code=200, + reason='OK', + headers={'Content-Range': '0-0/*'}, + json=self.data[0] + ) + # in order to fetch range since index 2 + mock.register_uri( + 'GET', + 'http://example.com/superhero', + request_headers={**DEFAULT_HEADERS, **{'Range-Unit': 'items', 'Range': '2-'}}, + status_code=200, + reason='OK', + headers={'Content-Range': '2-4/*'}, + json=self.data[2:] + ) + # in order to fetch length + mock.register_uri( + 'GET', + 'http://example.com/superhero', + request_headers={**DEFAULT_HEADERS, **{'Range-Unit': 'items', 'Range': '0-0', 'Prefer': 'count=exact'}}, + status_code=206, + reason='Partial Content', + headers={'Content-Range': '0-0/5'}, + json=self.data[0] + ) + res = self.pgrest_client.filter('superhero', count=Count.EXACT) + + self.assertIsInstance(res, LazyPostgrestJsonResult) # should return lazy object + self.assertFalse(mock.called) # no request should have been made yet + + self.assertEqual(len(res), len(self.data)) # should fetch len + self.assertTrue(mock.called_once) # should have been called once + self.assertEqual(res._len_cache, len(self.data)) # len of fetched data should be cached + self.assertListEqual(res[2:], self.data[2:]) # should fetch range starting at index 2 + self.assertDictEqual(res[0], self.data[0]) # should fetch first element as range + self.assertListEqual(list(res), self.data) # should fetch all elements + self.assertListEqual(res._result_cache, self.data) # should cache all elements + self.assertTrue(mock.called) # should have been called at least once + self.assertEqual(mock.call_count, 4) # should have only been called 4 times (fetch len, range, first and all)