home · contact · privacy
Slightly improve and re-organize Condition tests.
[plomtask] / tests / conditions.py
index dabcf06f767d02732b972d5b4bf40972f7d9fe66..9b3a40383ad21d959ce8884fd5c180039d317919 100644 (file)
@@ -1,86 +1,33 @@
 """Test Conditions module."""
 """Test Conditions module."""
-from unittest import TestCase
-from tests.utils import TestCaseWithDB, TestCaseWithServer
+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
+    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'])
-
-    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'])
+    default_init_kwargs = {'is_active': False}
+    test_versioneds = {'title': str, 'description': str}
 
 
-    def test_Condition_by_id(self) -> None:
-        """Test .by_id(), including creation."""
-        self.check_by_id()
-
-    def test_Condition_all(self) -> None:
-        """Test .all()."""
-        self.check_all()
-
-    def test_Condition_singularity(self) -> None:
-        """Test pointers made for single object keep pointing to it."""
-        self.check_singularity('is_active', True)
-
-    def test_Condition_remove(self) -> None:
+    def test_remove(self) -> None:
         """Test .remove() effects on DB and cache."""
         """Test .remove() effects on DB and cache."""
-        self.check_remove()
-        c = Condition(None)
+        super().test_remove()
         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')
@@ -95,20 +42,147 @@ class TestsWithDB(TestCaseWithDB):
 class TestsWithServer(TestCaseWithServer):
     """Module tests against our HTTP server/handler (and database)."""
 
 class TestsWithServer(TestCaseWithServer):
     """Module tests against our HTTP server/handler (and database)."""
 
-    def test_do_POST_condition(self) -> None:
-        """Test POST /condition and its effect on the database."""
-        form_data = {'title': 'foo', 'description': 'foo'}
-        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?id=', 404)
-        self.check_post(form_data, '/condition?id=2', 404)
-        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'}
-        self.check_post(form_data, '/condition', 302, '/condition?id=1')
+    @classmethod
+    def GET_condition_dict(cls, cond: dict[str, object]) -> dict[str, object]:
+        """Return JSON of GET /condition to expect."""
+        return {'is_new': False,
+                'enabled_processes': [],
+                'disabled_processes': [],
+                'enabling_processes': [],
+                'disabling_processes': [],
+                'condition': cond['id'],
+                '_library': {'Condition': cls.as_refs([cond])}}
+
+    @classmethod
+    def GET_conditions_dict(cls, conds: list[dict[str, object]]
+                            ) -> dict[str, object]:
+        """Return JSON of GET /conditions to expect."""
+        library = {'Condition': cls.as_refs(conds)} if conds else {}
+        d: dict[str, object] = {'conditions': cls.as_id_list(conds),
+                                'sort_by': 'title',
+                                'pattern': '',
+                                '_library': library}
+        return d
+
+    def test_fail_POST_condition(self) -> None:
+        """Test malformed/illegal POST /condition requests."""
+        # check invalid POST payloads
+        url = '/condition'
+        self.check_post({}, url, 400)
+        self.check_post({'title': ''}, url, 400)
+        self.check_post({'title': '', 'description': ''}, url, 400)
+        self.check_post({'title': '', 'is_active': False}, url, 400)
+        self.check_post({'description': '', 'is_active': False}, url, 400)
+        # check valid POST payload on bad paths
+        valid_payload = {'title': '', 'description': '', 'is_active': False}
+        self.check_post(valid_payload, '/condition?id=foo', 400)
+
+    def test_POST_condition(self) -> None:
+        """Test (valid) POST /condition and its effect on GET /condition[s]."""
+        # test valid POST's effect on …
+        post = {'title': 'foo', 'description': 'oof', 'is_active': False}
+        self.check_post(post, '/condition', 302, '/condition?id=1')
+        # … single /condition
+        cond = self.cond_as_dict(titles=['foo'], descriptions=['oof'])
+        assert isinstance(cond['_versioned'], dict)
+        expected_single = self.GET_condition_dict(cond)
+        self.check_json_get('/condition?id=1', expected_single)
+        # … full /conditions
+        expected_all = self.GET_conditions_dict([cond])
+        self.check_json_get('/conditions', expected_all)
+        # test (no) effect of invalid POST to existing Condition on /condition
+        self.check_post({}, '/condition?id=1', 400)
+        self.check_json_get('/condition?id=1', expected_single)
+        # test effect of POST changing title and activeness
+        post = {'title': 'bar', 'description': 'oof', 'is_active': True}
+        self.check_post(post, '/condition?id=1', 302)
+        cond['_versioned']['title'][1] = 'bar'
+        cond['is_active'] = True
+        self.check_json_get('/condition?id=1', expected_single)
+        # test deletion POST's effect on …
+        self.check_post({'delete': ''}, '/condition?id=1', 302, '/conditions')
+        cond = self.cond_as_dict()
+        assert isinstance(expected_single['_library'], dict)
+        expected_single['_library']['Condition'] = self.as_refs([cond])
+        self.check_json_get('/condition?id=1', expected_single)
+        # … full /conditions
+        expected_all['conditions'] = []
+        expected_all['_library'] = {}
+        self.check_json_get('/conditions', expected_all)
+
+    def test_GET_condition(self) -> None:
+        """More GET /condition testing, especially for Process relations."""
+        # check expected default status codes
         self.check_get_defaults('/condition')
         self.check_get_defaults('/condition')
-        self.check_get('/conditions', 200)
+        # make Condition and two Processes that among them establish all
+        # possible ConditionsRelations to it, …
+        cond_post = {'title': 'foo', 'description': 'oof', 'is_active': False}
+        self.check_post(cond_post, '/condition', 302, '/condition?id=1')
+        proc1_post = {'title': 'A', 'description': '', 'effort': 1.0,
+                      'conditions': [1], 'disables': [1]}
+        proc2_post = {'title': 'B', 'description': '', 'effort': 1.0,
+                      'enables': [1], 'blockers': [1]}
+        self.post_process(1, proc1_post)
+        self.post_process(2, proc2_post)
+        # … then check /condition displays all these properly.
+        cond = self.cond_as_dict(titles=['foo'], descriptions=['oof'])
+        assert isinstance(cond['id'], int)
+        proc1 = self.proc_as_dict(conditions=[cond['id']],
+                                  disables=[cond['id']])
+        proc2 = self.proc_as_dict(2, 'B',
+                                  blockers=[cond['id']],
+                                  enables=[cond['id']])
+        expected = self.GET_condition_dict(cond)
+        assert isinstance(expected['_library'], dict)
+        expected['enabled_processes'] = self.as_id_list([proc1])
+        expected['disabled_processes'] = self.as_id_list([proc2])
+        expected['enabling_processes'] = self.as_id_list([proc2])
+        expected['disabling_processes'] = self.as_id_list([proc1])
+        expected['_library']['Process'] = self.as_refs([proc1, proc2])
+        self.check_json_get('/condition?id=1', expected)
+
+    def test_GET_conditions(self) -> None:
+        """Test GET /conditions."""
+        # test empty result on empty DB, default-settings on empty params
+        expected = self.GET_conditions_dict([])
+        self.check_json_get('/conditions', expected)
+        # test on meaningless non-empty params (incl. entirely un-used key),
+        # that 'sort_by' default to 'title' (even if set to something else, as
+        # long as without handler) and 'pattern' get preserved
+        expected['pattern'] = 'bar'  # preserved despite zero effect!
+        url = '/conditions?sort_by=foo&pattern=bar&foo=x'
+        self.check_json_get(url, expected)
+        # test non-empty result, automatic (positive) sorting by title
+        post1 = {'is_active': False, 'title': 'foo', 'description': 'oof'}
+        post2 = {'is_active': False, 'title': 'bar', 'description': 'rab'}
+        post3 = {'is_active': True, 'title': 'baz', 'description': 'zab'}
+        self.check_post(post1, '/condition', 302, '/condition?id=1')
+        self.check_post(post2, '/condition', 302, '/condition?id=2')
+        self.check_post(post3, '/condition', 302, '/condition?id=3')
+        cond1 = self.cond_as_dict(1, False, ['foo'], ['oof'])
+        cond2 = self.cond_as_dict(2, False, ['bar'], ['rab'])
+        cond3 = self.cond_as_dict(3, True, ['baz'], ['zab'])
+        expected = self.GET_conditions_dict([cond2, cond3, cond1])
+        self.check_json_get('/conditions', expected)
+        # 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['sort_by'] = '-title'
+        expected['conditions'] = self.as_id_list([cond1, cond3, cond2])
+        self.check_json_get('/conditions?sort_by=-title', expected)
+        expected['sort_by'] = 'is_active'
+        expected['conditions'] = self.as_id_list([cond1, cond2, cond3])
+        self.check_json_get('/conditions?sort_by=is_active', expected)
+        expected['sort_by'] = '-is_active'
+        expected['conditions'] = self.as_id_list([cond3, cond1, cond2])
+        self.check_json_get('/conditions?sort_by=-is_active', expected)
+        # test pattern matching on title
+        expected = self.GET_conditions_dict([cond2, cond3])
+        expected['pattern'] = 'ba'
+        self.check_json_get('/conditions?pattern=ba', expected)
+        # test pattern matching on description
+        assert isinstance(expected['_library'], dict)
+        expected['conditions'] = self.as_id_list([cond1])
+        expected['_library']['Condition'] = self.as_refs([cond1])
+        expected['pattern'] = 'of'
+        self.check_json_get('/conditions?pattern=of', expected)