home · contact · privacy
Make use of new JSON interface for GET /conditions testing.
[plomtask] / tests / conditions.py
index dabcf06f767d02732b972d5b4bf40972f7d9fe66..a316d010b46ea0b648c2d725958586ddbdc026fa 100644 (file)
@@ -1,64 +1,30 @@
 """Test Conditions module."""
 """Test Conditions module."""
-from unittest import TestCase
-from tests.utils import TestCaseWithDB, TestCaseWithServer
+from json import loads as json_loads
+from tests.utils import TestCaseWithDB, TestCaseWithServer, TestCaseSansDB
 from plomtask.conditions import Condition
 from plomtask.processes import Process
 from plomtask.todos import Todo
 from plomtask.exceptions import HandledException
 
 
 from plomtask.conditions import Condition
 from plomtask.processes import Process
 from plomtask.todos import Todo
 from plomtask.exceptions import HandledException
 
 
-class TestsSansDB(TestCase):
+class TestsSansDB(TestCaseSansDB):
     """Tests requiring no DB setup."""
     """Tests requiring no DB setup."""
-
-    def test_Condition_id_setting(self) -> None:
-        """Test .id_ being set and its legal range being enforced."""
-        with self.assertRaises(HandledException):
-            Condition(0)
-        condition = Condition(5)
-        self.assertEqual(condition.id_, 5)
+    checked_class = Condition
+    do_id_test = True
+    versioned_defaults_to_test = {'title': 'UNNAMED', 'description': ''}
 
 
 class TestsWithDB(TestCaseWithDB):
     """Tests requiring DB, but not server setup."""
     checked_class = Condition
 
 
 class TestsWithDB(TestCaseWithDB):
     """Tests requiring DB, but not server setup."""
     checked_class = Condition
-    default_ids = (1, 2, 3)
-
-    def versioned_condition(self) -> Condition:
-        """Create Condition with some VersionedAttribute values."""
-        c = Condition(None)
-        c.title.set('title1')
-        c.title.set('title2')
-        c.description.set('desc1')
-        c.description.set('desc2')
-        return c
-
-    def test_Condition_saving_and_caching(self) -> None:
-        """Test .save/.save_core."""
-        kwargs = {'id_': 1, 'is_active': False}
-        self.check_saving_and_caching(**kwargs)
-        # check .id_ set if None, and versioned attributes too
-        c = self.versioned_condition()
-        c.save(self.db_conn)
-        self.assertEqual(c.id_, 2)
-        self.assertEqual(sorted(c.title.history.values()),
-                         ['title1', 'title2'])
-        self.assertEqual(sorted(c.description.history.values()),
-                         ['desc1', 'desc2'])
+    default_init_kwargs = {'is_active': False}
+    test_versioneds = {'title': str, 'description': str}
 
     def test_Condition_from_table_row(self) -> None:
         """Test .from_table_row() properly reads in class from DB"""
         self.check_from_table_row()
 
     def test_Condition_from_table_row(self) -> None:
         """Test .from_table_row() properly reads in class from DB"""
         self.check_from_table_row()
-        c = self.versioned_condition()
-        c.save(self.db_conn)
-        assert isinstance(c.id_, int)
-        for row in self.db_conn.row_where(Condition.table_name, 'id', c.id_):
-            retrieved = Condition.from_table_row(self.db_conn, row)
-            # pylint: disable=no-member
-            self.assertEqual(sorted(retrieved.title.history.values()),
-                             ['title1', 'title2'])
-            # pylint: disable=no-member
-            self.assertEqual(sorted(retrieved.description.history.values()),
-                             ['desc1', 'desc2'])
+        self.check_versioned_from_table_row('title', str)
+        self.check_versioned_from_table_row('description', str)
 
     def test_Condition_by_id(self) -> None:
         """Test .by_id(), including creation."""
 
     def test_Condition_by_id(self) -> None:
         """Test .by_id(), including creation."""
@@ -72,15 +38,20 @@ class TestsWithDB(TestCaseWithDB):
         """Test pointers made for single object keep pointing to it."""
         self.check_singularity('is_active', True)
 
         """Test pointers made for single object keep pointing to it."""
         self.check_singularity('is_active', True)
 
+    def test_Condition_versioned_attributes_singularity(self) -> None:
+        """Test behavior of VersionedAttributes on saving (with .title)."""
+        self.check_versioned_singularity()
+
     def test_Condition_remove(self) -> None:
         """Test .remove() effects on DB and cache."""
         self.check_remove()
     def test_Condition_remove(self) -> None:
         """Test .remove() effects on DB and cache."""
         self.check_remove()
-        c = Condition(None)
         proc = Process(None)
         proc = Process(None)
+        proc.save(self.db_conn)
         todo = Todo(None, proc, False, '2024-01-01')
         for depender in (proc, todo):
             assert hasattr(depender, 'save')
             assert hasattr(depender, 'set_conditions')
         todo = Todo(None, proc, False, '2024-01-01')
         for depender in (proc, todo):
             assert hasattr(depender, 'save')
             assert hasattr(depender, 'set_conditions')
+            c = Condition(None)
             c.save(self.db_conn)
             depender.save(self.db_conn)
             depender.set_conditions(self.db_conn, [c.id_], 'conditions')
             c.save(self.db_conn)
             depender.save(self.db_conn)
             depender.set_conditions(self.db_conn, [c.id_], 'conditions')
@@ -97,7 +68,7 @@ class TestsWithServer(TestCaseWithServer):
 
     def test_do_POST_condition(self) -> None:
         """Test POST /condition and its effect on the database."""
 
     def test_do_POST_condition(self) -> None:
         """Test POST /condition and its effect on the database."""
-        form_data = {'title': 'foo', 'description': 'foo'}
+        form_data = {'title': 'foo', 'description': 'foo', 'is_active': False}
         self.check_post(form_data, '/condition', 302, '/condition?id=1')
         self.assertEqual(1, len(Condition.all(self.db_conn)))
         form_data['delete'] = ''
         self.check_post(form_data, '/condition', 302, '/condition?id=1')
         self.assertEqual(1, len(Condition.all(self.db_conn)))
         form_data['delete'] = ''
@@ -106,9 +77,80 @@ class TestsWithServer(TestCaseWithServer):
         self.check_post(form_data, '/condition?id=1', 302, '/conditions')
         self.assertEqual(0, len(Condition.all(self.db_conn)))
 
         self.check_post(form_data, '/condition?id=1', 302, '/conditions')
         self.assertEqual(0, len(Condition.all(self.db_conn)))
 
-    def test_do_GET(self) -> None:
-        """Test /condition and /conditions response codes."""
-        form_data = {'title': 'foo', 'description': 'foo'}
+    def test_do_GET_condition(self) -> None:
+        """Test GET /condition."""
+        form_data = {'title': 'foo', 'description': 'foo', 'is_active': False}
         self.check_post(form_data, '/condition', 302, '/condition?id=1')
         self.check_get_defaults('/condition')
         self.check_post(form_data, '/condition', 302, '/condition?id=1')
         self.check_get_defaults('/condition')
-        self.check_get('/conditions', 200)
+
+    def test_do_GET_conditions(self) -> None:
+        """Test GET /conditions."""
+
+        def check(params: str, expected_json: dict[str, object]) -> None:
+            self.conn.request('GET', f'/conditions{params}')
+            response = self.conn.getresponse()
+            self.assertEqual(response.status, 200)
+            retrieved_json = json_loads(response.read().decode())
+            # ignore history timestamps (too difficult too anticipate)
+            for cond in retrieved_json['conditions']:
+                for k in [k for k in cond.keys() if isinstance(cond[k], dict)]:
+                    history = cond[k]['history']
+                    history = {'[IGNORE]': list(history.values())[0]}
+                    cond[k]['history'] = history
+            self.assertEqual(expected_json, retrieved_json)
+
+        # test empty result on empty DB, default-settings on empty params
+        expected_json: dict[str, object] = {'conditions': [],
+                                            'sort_by': 'title',
+                                            'pattern': ''}
+        check('', expected_json)
+        # test on meaningless non-empty params (incl. entirely un-used key)
+        expected_json = {'conditions': [],
+                         'sort_by': 'title',  # nonsense "foo" defaulting
+                         'pattern': 'bar'}  # preserved despite zero effect
+        check('?sort_by=foo&pattern=bar&foo=x', expected_json)
+        # test non-empty result, automatic (positive) sorting by title
+        post_1 = {'title': 'foo', 'description': 'oof', 'is_active': False}
+        self.check_post(post_1, '/condition', 302, '/condition?id=1')
+        post_2 = {'title': 'bar', 'description': 'rab', 'is_active': False}
+        self.check_post(post_2, '/condition', 302, '/condition?id=2')
+        post_3 = {'title': 'baz', 'description': 'zab', 'is_active': True}
+        self.check_post(post_3, '/condition', 302, '/condition?id=3')
+        cond_1 = {'id': 1, 'is_active': False,
+                  'title': {'history': {'[IGNORE]': 'foo'},
+                            'parent_id': 1},
+                           'description': {'history': {'[IGNORE]': 'oof'},
+                                           'parent_id': 1}}
+        cond_2 = {'id': 2, 'is_active': False,
+                  'title': {'history': {'[IGNORE]': 'bar'},
+                            'parent_id': 2},
+                  'description': {'history': {'[IGNORE]': 'rab'},
+                                  'parent_id': 2}}
+        cond_3 = {'id': 3, 'is_active': True,
+                  'title': {'history': {'[IGNORE]': 'baz'},
+                            'parent_id': 3},
+                  'description': {'history': {'[IGNORE]': 'zab'},
+                                  'parent_id': 3}}
+        cons = [cond_2, cond_3, cond_1]
+        expected_json = {'conditions': cons, 'sort_by': 'title', 'pattern': ''}
+        check('', expected_json)
+        # test other sortings
+        # (NB: by .is_active has two items of =False, their order currently
+        # is not explicitly made predictable, so mail fail until we do)
+        expected_json['conditions'] = [cond_1, cond_3, cond_2]
+        expected_json['sort_by'] = '-title'
+        check('?sort_by=-title', expected_json)
+        expected_json['conditions'] = [cond_1, cond_2, cond_3]
+        expected_json['sort_by'] = 'is_active'
+        check('?sort_by=is_active', expected_json)
+        expected_json['conditions'] = [cond_3, cond_1, cond_2]
+        expected_json['sort_by'] = '-is_active'
+        check('?sort_by=-is_active', expected_json)
+        # test pattern matching on title
+        expected_json = {'conditions': [cond_2, cond_3],
+                         'sort_by': 'title', 'pattern': 'ba'}
+        check('?pattern=ba', expected_json)
+        # test pattern matching on description
+        expected_json['conditions'] = [cond_1]
+        expected_json['pattern'] = 'oo'
+        check('?pattern=oo', expected_json)