home · contact · privacy
Slightly improve and re-organize Condition tests.
[plomtask] / tests / processes.py
index 127a3f680d4a674030b1be83756f12921a78d4e5..1b20e217d077d826765f5a83c9a2b3250de38ba2 100644 (file)
@@ -1,4 +1,5 @@
 """Test Processes module."""
 """Test Processes module."""
+from typing import Any
 from tests.utils import TestCaseWithDB, TestCaseWithServer, TestCaseSansDB
 from plomtask.processes import Process, ProcessStep, ProcessStepsNode
 from plomtask.conditions import Condition
 from tests.utils import TestCaseWithDB, TestCaseWithServer, TestCaseSansDB
 from plomtask.processes import Process, ProcessStep, ProcessStepsNode
 from plomtask.conditions import Condition
@@ -9,7 +10,6 @@ from plomtask.todos import Todo
 class TestsSansDB(TestCaseSansDB):
     """Module tests not requiring DB setup."""
     checked_class = Process
 class TestsSansDB(TestCaseSansDB):
     """Module tests not requiring DB setup."""
     checked_class = Process
-    do_id_test = True
     versioned_defaults_to_test = {'title': 'UNNAMED', 'description': '',
                                   'effort': 1.0}
 
     versioned_defaults_to_test = {'title': 'UNNAMED', 'description': '',
                                   'effort': 1.0}
 
@@ -17,7 +17,6 @@ class TestsSansDB(TestCaseSansDB):
 class TestsSansDBProcessStep(TestCaseSansDB):
     """Module tests not requiring DB setup."""
     checked_class = ProcessStep
 class TestsSansDBProcessStep(TestCaseSansDB):
     """Module tests not requiring DB setup."""
     checked_class = ProcessStep
-    do_id_test = True
     default_init_args = [2, 3, 4]
 
 
     default_init_args = [2, 3, 4]
 
 
@@ -58,24 +57,20 @@ class TestsWithDB(TestCaseWithDB):
     def test_Process_conditions_saving(self) -> None:
         """Test .save/.save_core."""
         p, set1, set2, set3 = self.p_of_conditions()
     def test_Process_conditions_saving(self) -> None:
         """Test .save/.save_core."""
         p, set1, set2, set3 = self.p_of_conditions()
-        p.uncache()
+        assert p.id_ is not None
         r = Process.by_id(self.db_conn, p.id_)
         self.assertEqual(sorted(r.conditions), sorted(set1))
         self.assertEqual(sorted(r.enables), sorted(set2))
         self.assertEqual(sorted(r.disables), sorted(set3))
 
         r = Process.by_id(self.db_conn, p.id_)
         self.assertEqual(sorted(r.conditions), sorted(set1))
         self.assertEqual(sorted(r.enables), sorted(set2))
         self.assertEqual(sorted(r.disables), sorted(set3))
 
-    def test_Process_from_table_row(self) -> None:
-        """Test .from_table_row() properly reads in class from DB"""
-        self.check_from_table_row()
-        self.check_versioned_from_table_row('title', str)
-        self.check_versioned_from_table_row('description', str)
-        self.check_versioned_from_table_row('effort', float)
+    def test_from_table_row(self) -> None:
+        """Test .from_table_row() properly reads in class from DB."""
+        super().test_from_table_row()
         p, set1, set2, set3 = self.p_of_conditions()
         p.save(self.db_conn)
         assert isinstance(p.id_, int)
         for row in self.db_conn.row_where(self.checked_class.table_name,
                                           'id', p.id_):
         p, set1, set2, set3 = self.p_of_conditions()
         p.save(self.db_conn)
         assert isinstance(p.id_, int)
         for row in self.db_conn.row_where(self.checked_class.table_name,
                                           'id', p.id_):
-            # pylint: disable=no-member
             r = Process.from_table_row(self.db_conn, row)
             self.assertEqual(sorted(r.conditions), sorted(set1))
             self.assertEqual(sorted(r.enables), sorted(set2))
             r = Process.from_table_row(self.db_conn, row)
             self.assertEqual(sorted(r.conditions), sorted(set1))
             self.assertEqual(sorted(r.enables), sorted(set2))
@@ -83,44 +78,86 @@ class TestsWithDB(TestCaseWithDB):
 
     def test_Process_steps(self) -> None:
         """Test addition, nesting, and non-recursion of ProcessSteps"""
 
     def test_Process_steps(self) -> None:
         """Test addition, nesting, and non-recursion of ProcessSteps"""
-        def add_step(proc: Process,
-                     steps_proc: list[tuple[int | None, int, int | None]],
-                     step_tuple: tuple[int | None, int, int | None],
-                     expected_id: int) -> None:
-            steps_proc += [step_tuple]
-            proc.set_steps(self.db_conn, steps_proc)
-            steps_proc[-1] = (expected_id, step_tuple[1], step_tuple[2])
+        # pylint: disable=too-many-locals
+        # pylint: disable=too-many-statements
         p1, p2, p3 = self.three_processes()
         assert isinstance(p1.id_, int)
         assert isinstance(p2.id_, int)
         assert isinstance(p3.id_, int)
         p1, p2, p3 = self.three_processes()
         assert isinstance(p1.id_, int)
         assert isinstance(p2.id_, int)
         assert isinstance(p3.id_, int)
-        steps_p1: list[tuple[int | None, int, int | None]] = []
-        add_step(p1, steps_p1, (None, p2.id_, None), 1)
+        steps_p1: list[ProcessStep] = []
+        # add step of process p2 as first (top-level) step to p1
+        s_p2_to_p1 = ProcessStep(None, p1.id_, p2.id_, None)
+        steps_p1 += [s_p2_to_p1]
+        p1.set_steps(self.db_conn, steps_p1)
         p1_dict: dict[int, ProcessStepsNode] = {}
         p1_dict: dict[int, ProcessStepsNode] = {}
-        p1_dict[1] = ProcessStepsNode(p2, None, True, {}, False)
+        p1_dict[1] = ProcessStepsNode(p2, None, True, {})
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
-        add_step(p1, steps_p1, (None, p3.id_, None), 2)
-        step_2 = p1.explicit_steps[-1]
-        p1_dict[2] = ProcessStepsNode(p3, None, True, {}, False)
+        # add step of process p3 as second (top-level) step to p1
+        s_p3_to_p1 = ProcessStep(None, p1.id_, p3.id_, None)
+        steps_p1 += [s_p3_to_p1]
+        p1.set_steps(self.db_conn, steps_p1)
+        p1_dict[2] = ProcessStepsNode(p3, None, True, {})
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
-        steps_p2: list[tuple[int | None, int, int | None]] = []
-        add_step(p2, steps_p2, (None, p3.id_, None), 3)
-        p1_dict[1].steps[3] = ProcessStepsNode(p3, None, False, {}, False)
+        # add step of process p3 as first (top-level) step to p2,
+        steps_p2: list[ProcessStep] = []
+        s_p3_to_p2 = ProcessStep(None, p2.id_, p3.id_, None)
+        steps_p2 += [s_p3_to_p2]
+        p2.set_steps(self.db_conn, steps_p2)
+        # expect it as implicit sub-step of p1's second (p3) step
+        p2_dict = {3: ProcessStepsNode(p3, None, False, {})}
+        p1_dict[1].steps[3] = p2_dict[3]
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
-        add_step(p1, steps_p1, (None, p2.id_, step_2.id_), 4)
-        step_3 = ProcessStepsNode(p3, None, False, {}, True)
-        p1_dict[2].steps[4] = ProcessStepsNode(p2, step_2.id_, True,
-                                               {3: step_3}, False)
+        # add step of process p2 as explicit sub-step to p1's second sub-step
+        s_p2_to_p1_first = ProcessStep(None, p1.id_, p2.id_, s_p3_to_p1.id_)
+        steps_p1 += [s_p2_to_p1_first]
+        p1.set_steps(self.db_conn, steps_p1)
+        seen_3 = ProcessStepsNode(p3, None, False, {}, False)
+        p1_dict[1].steps[3].seen = True
+        p1_dict[2].steps[4] = ProcessStepsNode(p2, s_p3_to_p1.id_, True,
+                                               {3: seen_3})
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
-        add_step(p1, steps_p1, (None, p3.id_, 999), 5)
-        p1_dict[5] = ProcessStepsNode(p3, None, True, {}, False)
+        # add step of process p3 as explicit sub-step to non-existing p1
+        # sub-step (of id=999), expect it to become another p1 top-level step
+        s_p3_to_p1_999 = ProcessStep(None, p1.id_, p3.id_, 999)
+        steps_p1 += [s_p3_to_p1_999]
+        p1.set_steps(self.db_conn, steps_p1)
+        p1_dict[5] = ProcessStepsNode(p3, None, True, {})
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
-        add_step(p1, steps_p1, (None, p3.id_, 3), 6)
-        p1_dict[6] = ProcessStepsNode(p3, None, True, {}, False)
+        # add step of process p3 as explicit sub-step to p1's implicit p3
+        # sub-step, expect it to become another p1 top-level step
+        s_p3_to_p1_impl_p3 = ProcessStep(None, p1.id_, p3.id_, s_p3_to_p2.id_)
+        steps_p1 += [s_p3_to_p1_impl_p3]
+        p1.set_steps(self.db_conn, steps_p1)
+        p1_dict[6] = ProcessStepsNode(p3, None, True, {})
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         self.assertEqual(p1.used_as_step_by(self.db_conn), [])
         self.assertEqual(p2.used_as_step_by(self.db_conn), [p1])
         self.assertEqual(p3.used_as_step_by(self.db_conn), [p1, p2])
         self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
         self.assertEqual(p1.used_as_step_by(self.db_conn), [])
         self.assertEqual(p2.used_as_step_by(self.db_conn), [p1])
         self.assertEqual(p3.used_as_step_by(self.db_conn), [p1, p2])
+        # # add step of process p3 as explicit sub-step to p1's first sub-step,
+        # # expect it to eliminate implicit p3 sub-step
+        # s_p3_to_p1_first_explicit = ProcessStep(None, p1.id_, p3.id_,
+        #                                         s_p2_to_p1.id_)
+        # p1_dict[1].steps = {7: ProcessStepsNode(p3, 1, True, {})}
+        # p1_dict[2].steps[4].steps[3].seen = False
+        # steps_p1 += [s_p3_to_p1_first_explicit]
+        # p1.set_steps(self.db_conn, steps_p1)
+        # self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
+        # ensure implicit steps non-top explicit steps are shown
+        s_p3_to_p2_first = ProcessStep(None, p2.id_, p3.id_, s_p3_to_p2.id_)
+        steps_p2 += [s_p3_to_p2_first]
+        p2.set_steps(self.db_conn, steps_p2)
+        p1_dict[1].steps[3].steps[7] = ProcessStepsNode(p3, 3, False, {}, True)
+        p1_dict[2].steps[4].steps[3].steps[7] = ProcessStepsNode(p3, 3, False,
+                                                                 {}, False)
+        self.assertEqual(p1.get_steps(self.db_conn, None), p1_dict)
+        # ensure suppressed step nodes are hidden
+        assert isinstance(s_p3_to_p2.id_, int)
+        p1.set_step_suppressions(self.db_conn, [s_p3_to_p2.id_])
+        p1_dict[1].steps[3].steps = {}
+        p1_dict[1].steps[3].is_suppressed = True
+        p1_dict[2].steps[4].steps[3].steps = {}
+        p1_dict[2].steps[4].steps[3].is_suppressed = True
+        self.assertEqual(p1.get_steps(self.db_conn), p1_dict)
 
     def test_Process_conditions(self) -> None:
         """Test setting Process.conditions/enables/disables."""
 
     def test_Process_conditions(self) -> None:
         """Test setting Process.conditions/enables/disables."""
@@ -142,42 +179,30 @@ class TestsWithDB(TestCaseWithDB):
             method(self.db_conn, [c1.id_, c2.id_])
             self.assertEqual(getattr(p, target), [c1, c2])
 
             method(self.db_conn, [c1.id_, c2.id_])
             self.assertEqual(getattr(p, target), [c1, c2])
 
-    def test_Process_by_id(self) -> None:
-        """Test .by_id(), including creation"""
-        self.check_by_id()
-
-    def test_Process_all(self) -> None:
-        """Test .all()."""
-        self.check_all()
-
-    def test_Process_singularity(self) -> None:
-        """Test pointers made for single object keep pointing to it."""
-        self.check_singularity('conditions', [Condition(None)])
-
-    def test_Process_versioned_attributes_singularity(self) -> None:
-        """Test behavior of VersionedAttributes on saving (with .title)."""
-        self.check_versioned_singularity()
-
-    def test_Process_removal(self) -> None:
+    def test_remove(self) -> None:
         """Test removal of Processes and ProcessSteps."""
         """Test removal of Processes and ProcessSteps."""
-        self.check_remove()
+        super().test_remove()
         p1, p2, p3 = self.three_processes()
         assert isinstance(p1.id_, int)
         assert isinstance(p2.id_, int)
         assert isinstance(p3.id_, int)
         p1, p2, p3 = self.three_processes()
         assert isinstance(p1.id_, int)
         assert isinstance(p2.id_, int)
         assert isinstance(p3.id_, int)
-        p2.set_steps(self.db_conn, [(None, p1.id_, None)])
+        step = ProcessStep(None, p2.id_, p1.id_, None)
+        p2.set_steps(self.db_conn, [step])
+        step_id = step.id_
         with self.assertRaises(HandledException):
             p1.remove(self.db_conn)
         with self.assertRaises(HandledException):
             p1.remove(self.db_conn)
-        step = p2.explicit_steps[0]
         p2.set_steps(self.db_conn, [])
         with self.assertRaises(NotFoundException):
         p2.set_steps(self.db_conn, [])
         with self.assertRaises(NotFoundException):
-            ProcessStep.by_id(self.db_conn, step.id_)
+            assert step_id is not None
+            ProcessStep.by_id(self.db_conn, step_id)
         p1.remove(self.db_conn)
         p1.remove(self.db_conn)
-        p2.set_steps(self.db_conn, [(None, p3.id_, None)])
-        step = p2.explicit_steps[0]
+        step = ProcessStep(None, p2.id_, p3.id_, None)
+        p2.set_steps(self.db_conn, [step])
+        step_id = step.id_
         p2.remove(self.db_conn)
         with self.assertRaises(NotFoundException):
         p2.remove(self.db_conn)
         with self.assertRaises(NotFoundException):
-            ProcessStep.by_id(self.db_conn, step.id_)
+            assert step_id is not None
+            ProcessStep.by_id(self.db_conn, step_id)
         todo = Todo(None, p3, False, '2024-01-01')
         todo.save(self.db_conn)
         with self.assertRaises(HandledException):
         todo = Todo(None, p3, False, '2024-01-01')
         todo.save(self.db_conn)
         with self.assertRaises(HandledException):
@@ -189,29 +214,25 @@ class TestsWithDB(TestCaseWithDB):
 class TestsWithDBForProcessStep(TestCaseWithDB):
     """Module tests requiring DB setup."""
     checked_class = ProcessStep
 class TestsWithDBForProcessStep(TestCaseWithDB):
     """Module tests requiring DB setup."""
     checked_class = ProcessStep
-    default_init_kwargs = {'owner_id': 2, 'step_process_id': 3,
-                           'parent_step_id': 4}
-
-    def test_ProcessStep_from_table_row(self) -> None:
-        """Test .from_table_row() properly reads in class from DB"""
-        self.check_from_table_row(2, 3, None)
+    default_init_kwargs = {'owner_id': 1, 'step_process_id': 2,
+                           'parent_step_id': 3}
 
 
-    def test_ProcessStep_singularity(self) -> None:
-        """Test pointers made for single object keep pointing to it."""
-        self.check_singularity('parent_step_id', 1, 2, 3, None)
+    def setUp(self) -> None:
+        super().setUp()
+        self.p1 = Process(1)
+        self.p1.save(self.db_conn)
 
 
-    def test_ProcessStep_remove(self) -> None:
+    def test_remove(self) -> None:
         """Test .remove and unsetting of owner's .explicit_steps entry."""
         """Test .remove and unsetting of owner's .explicit_steps entry."""
-        p1 = Process(None)
-        p2 = Process(None)
-        p1.save(self.db_conn)
+        p2 = Process(2)
         p2.save(self.db_conn)
         p2.save(self.db_conn)
+        assert isinstance(self.p1.id_, int)
         assert isinstance(p2.id_, int)
         assert isinstance(p2.id_, int)
-        p1.set_steps(self.db_conn, [(None, p2.id_, None)])
-        step = p1.explicit_steps[0]
+        step = ProcessStep(None, self.p1.id_, p2.id_, None)
+        self.p1.set_steps(self.db_conn, [step])
         step.remove(self.db_conn)
         step.remove(self.db_conn)
-        self.assertEqual(p1.explicit_steps, [])
-        self.check_storage([])
+        self.assertEqual(self.p1.explicit_steps, [])
+        self.check_identity_with_cache_and_db([])
 
 
 class TestsWithServer(TestCaseWithServer):
 
 
 class TestsWithServer(TestCaseWithServer):
@@ -231,11 +252,12 @@ class TestsWithServer(TestCaseWithServer):
                         '/process?id=', 400)
         self.assertEqual(1, len(Process.all(self.db_conn)))
         form_data = {'title': 'foo', 'description': 'foo', 'effort': 1.0}
                         '/process?id=', 400)
         self.assertEqual(1, len(Process.all(self.db_conn)))
         form_data = {'title': 'foo', 'description': 'foo', 'effort': 1.0}
-        self.post_process(2, form_data | {'condition': []})
-        self.check_post(form_data | {'condition': [1]}, '/process?id=', 404)
-        self.check_post({'title': 'foo', 'description': 'foo'},
+        self.post_process(2, form_data | {'conditions': []})
+        self.check_post(form_data | {'conditions': [1]}, '/process?id=', 404)
+        self.check_post({'title': 'foo', 'description': 'foo',
+                         'is_active': False},
                         '/condition', 302, '/condition?id=1')
                         '/condition', 302, '/condition?id=1')
-        self.post_process(3, form_data | {'condition': [1]})
+        self.post_process(3, form_data | {'conditions': [1]})
         self.post_process(4, form_data | {'disables': [1]})
         self.post_process(5, form_data | {'enables': [1]})
         form_data['delete'] = ''
         self.post_process(4, form_data | {'disables': [1]})
         self.post_process(5, form_data | {'enables': [1]})
         form_data['delete'] = ''
@@ -245,23 +267,30 @@ class TestsWithServer(TestCaseWithServer):
 
     def test_do_POST_process_steps(self) -> None:
         """Test behavior of ProcessStep posting."""
 
     def test_do_POST_process_steps(self) -> None:
         """Test behavior of ProcessStep posting."""
+        # pylint: disable=too-many-statements
         form_data_1 = self.post_process(1)
         self.post_process(2)
         self.post_process(3)
         form_data_1 = self.post_process(1)
         self.post_process(2)
         self.post_process(3)
+        # post first (top-level) step of process 2 to process 1
         form_data_1['new_top_step'] = [2]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
         retrieved_step = retrieved_process.explicit_steps[0]
         form_data_1['new_top_step'] = [2]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
         retrieved_step = retrieved_process.explicit_steps[0]
+        retrieved_step_id = retrieved_step.id_
         self.assertEqual(retrieved_step.step_process_id, 2)
         self.assertEqual(retrieved_step.owner_id, 1)
         self.assertEqual(retrieved_step.parent_step_id, None)
         self.assertEqual(retrieved_step.step_process_id, 2)
         self.assertEqual(retrieved_step.owner_id, 1)
         self.assertEqual(retrieved_step.parent_step_id, None)
+        # post empty steps list to process, expect clean slate, and old step to
+        # completely disappear
         form_data_1['new_top_step'] = []
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(retrieved_process.explicit_steps, [])
         form_data_1['new_top_step'] = []
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(retrieved_process.explicit_steps, [])
+        assert retrieved_step_id is not None
         with self.assertRaises(NotFoundException):
         with self.assertRaises(NotFoundException):
-            ProcessStep.by_id(self.db_conn, retrieved_step.id_)
+            ProcessStep.by_id(self.db_conn, retrieved_step_id)
+        # post new first (top_level) step of process 3 to process 1
         form_data_1['new_top_step'] = [3]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         form_data_1['new_top_step'] = [3]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
@@ -269,18 +298,22 @@ class TestsWithServer(TestCaseWithServer):
         self.assertEqual(retrieved_step.step_process_id, 3)
         self.assertEqual(retrieved_step.owner_id, 1)
         self.assertEqual(retrieved_step.parent_step_id, None)
         self.assertEqual(retrieved_step.step_process_id, 3)
         self.assertEqual(retrieved_step.owner_id, 1)
         self.assertEqual(retrieved_step.parent_step_id, None)
+        # post to process steps list without keeps, expect clean slate
         form_data_1['new_top_step'] = []
         form_data_1['steps'] = [retrieved_step.id_]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(retrieved_process.explicit_steps, [])
         form_data_1['new_top_step'] = []
         form_data_1['steps'] = [retrieved_step.id_]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(retrieved_process.explicit_steps, [])
+        # post to process empty steps list but keep, expect 400
         form_data_1['steps'] = []
         form_data_1['steps'] = []
-        form_data_1['keep_step'] = [retrieved_step.id_]
+        form_data_1['keep_step'] = [retrieved_step_id]
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
-        form_data_1['steps'] = [retrieved_step.id_]
-        form_data_1['keep_step'] = [retrieved_step.id_]
+        # post to process steps list with keep on non-created step, expect 400
+        form_data_1['steps'] = [retrieved_step_id]
+        form_data_1['keep_step'] = [retrieved_step_id]
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
-        form_data_1[f'step_{retrieved_step.id_}_process_id'] = [2]
+        # post to process steps list with keep and process ID, expect 200
+        form_data_1[f'step_{retrieved_step_id}_process_id'] = [2]
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
         self.post_process(1, form_data_1)
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
@@ -288,14 +321,190 @@ class TestsWithServer(TestCaseWithServer):
         self.assertEqual(retrieved_step.step_process_id, 2)
         self.assertEqual(retrieved_step.owner_id, 1)
         self.assertEqual(retrieved_step.parent_step_id, None)
         self.assertEqual(retrieved_step.step_process_id, 2)
         self.assertEqual(retrieved_step.owner_id, 1)
         self.assertEqual(retrieved_step.parent_step_id, None)
-        form_data_1['new_top_step'] = ['foo']
-        form_data_1['steps'] = []
+        # post nonsense, expect 400 and preservation of previous state
+        form_data_1['steps'] = ['foo']
         form_data_1['keep_step'] = []
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
         form_data_1['keep_step'] = []
         self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
         retrieved_process = Process.by_id(self.db_conn, 1)
         self.assertEqual(len(retrieved_process.explicit_steps), 1)
+        retrieved_step = retrieved_process.explicit_steps[0]
+        self.assertEqual(retrieved_step.step_process_id, 2)
+        self.assertEqual(retrieved_step.owner_id, 1)
+        self.assertEqual(retrieved_step.parent_step_id, None)
+        # post to process steps list with keep and process ID, expect 200
+        form_data_1['new_top_step'] = [3]
+        form_data_1['steps'] = [retrieved_step.id_]
+        form_data_1['keep_step'] = [retrieved_step.id_]
+        self.post_process(1, form_data_1)
+        retrieved_process = Process.by_id(self.db_conn, 1)
+        self.assertEqual(len(retrieved_process.explicit_steps), 2)
+        retrieved_step_0 = retrieved_process.explicit_steps[1]
+        self.assertEqual(retrieved_step_0.step_process_id, 3)
+        self.assertEqual(retrieved_step_0.owner_id, 1)
+        self.assertEqual(retrieved_step_0.parent_step_id, None)
+        retrieved_step_1 = retrieved_process.explicit_steps[0]
+        self.assertEqual(retrieved_step_1.step_process_id, 2)
+        self.assertEqual(retrieved_step_1.owner_id, 1)
+        self.assertEqual(retrieved_step_1.parent_step_id, None)
+        # post to process steps list with keeps etc., but trigger recursion
+        form_data_1['new_top_step'] = []
+        form_data_1['steps'] = [retrieved_step_0.id_, retrieved_step_1.id_]
+        form_data_1['keep_step'] = [retrieved_step_0.id_, retrieved_step_1.id_]
+        form_data_1[f'step_{retrieved_step_0.id_}_process_id'] = [2]
+        form_data_1[f'step_{retrieved_step_1.id_}_process_id'] = [1]
+        self.check_post(form_data_1, '/process?id=1', 400, '/process?id=1')
+        # check previous status preserved despite failed steps setting
+        retrieved_process = Process.by_id(self.db_conn, 1)
+        self.assertEqual(len(retrieved_process.explicit_steps), 2)
+        retrieved_step_0 = retrieved_process.explicit_steps[0]
+        self.assertEqual(retrieved_step_0.step_process_id, 2)
+        self.assertEqual(retrieved_step_0.owner_id, 1)
+        self.assertEqual(retrieved_step_0.parent_step_id, None)
+        retrieved_step_1 = retrieved_process.explicit_steps[1]
+        self.assertEqual(retrieved_step_1.step_process_id, 3)
+        self.assertEqual(retrieved_step_1.owner_id, 1)
+        self.assertEqual(retrieved_step_1.parent_step_id, None)
+        # post sub-step to step
+        form_data_1[f'step_{retrieved_step_0.id_}_process_id'] = [3]
+        form_data_1[f'new_step_to_{retrieved_step_0.id_}'] = [3]
+        self.post_process(1, form_data_1)
+        retrieved_process = Process.by_id(self.db_conn, 1)
+        self.assertEqual(len(retrieved_process.explicit_steps), 3)
+        retrieved_step_0 = retrieved_process.explicit_steps[1]
+        self.assertEqual(retrieved_step_0.step_process_id, 2)
+        self.assertEqual(retrieved_step_0.owner_id, 1)
+        self.assertEqual(retrieved_step_0.parent_step_id, None)
+        retrieved_step_1 = retrieved_process.explicit_steps[0]
+        self.assertEqual(retrieved_step_1.step_process_id, 3)
+        self.assertEqual(retrieved_step_1.owner_id, 1)
+        self.assertEqual(retrieved_step_1.parent_step_id, None)
+        retrieved_step_2 = retrieved_process.explicit_steps[2]
+        self.assertEqual(retrieved_step_2.step_process_id, 3)
+        self.assertEqual(retrieved_step_2.owner_id, 1)
+        self.assertEqual(retrieved_step_2.parent_step_id, retrieved_step_1.id_)
 
     def test_do_GET(self) -> None:
         """Test /process and /processes response codes."""
 
     def test_do_GET(self) -> None:
         """Test /process and /processes response codes."""
+        self.check_get('/process', 200)
+        self.check_get('/process?id=', 200)
+        self.check_get('/process?id=1', 200)
         self.check_get_defaults('/process')
         self.check_get('/processes', 200)
         self.check_get_defaults('/process')
         self.check_get('/processes', 200)
+
+    def test_fail_GET_process(self) -> None:
+        """Test invalid GET /process params."""
+        # check for invalid IDs
+        self.check_get('/process?id=foo', 400)
+        self.check_get('/process?id=0', 500)
+        # check we catch invalid base64
+        self.check_get('/process?title_b64=foo', 400)
+        # check failure on references to unknown processes; we create Process
+        # of ID=1 here so we know the 404 comes from step_to=2 etc. (that tie
+        # the Process displayed by /process to others), not from not finding
+        # the main Process itself
+        self.post_process(1)
+        self.check_get('/process?id=1&step_to=2', 404)
+        self.check_get('/process?id=1&has_step=2', 404)
+
+    @classmethod
+    def GET_processes_dict(cls, procs: list[dict[str, object]]
+                           ) -> dict[str, object]:
+        """Return JSON of GET /processes to expect."""
+        library = {'Process': cls.as_refs(procs)} if procs else {}
+        d: dict[str, object] = {'processes': cls.as_id_list(procs),
+                                'sort_by': 'title',
+                                'pattern': '',
+                                '_library': library}
+        return d
+
+    @staticmethod
+    def procstep_as_dict(id_: int,
+                         owner_id: int,
+                         step_process_id: int,
+                         parent_step_id: int | None = None
+                         ) -> dict[str, object]:
+        """Return JSON of Process to expect."""
+        return {'id': id_,
+                'owner_id': owner_id,
+                'step_process_id': step_process_id,
+                'parent_step_id': parent_step_id}
+
+    def test_GET_processes(self) -> None:
+        """Test GET /processes."""
+        # pylint: disable=too-many-statements
+        # test empty result on empty DB, default-settings on empty params
+        expected = self.GET_processes_dict([])
+        self.check_json_get('/processes', 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 = '/processes?sort_by=foo&pattern=bar&foo=x'
+        self.check_json_get(url, expected)
+        # test non-empty result, automatic (positive) sorting by title
+        post1: dict[str, Any]
+        post2: dict[str, Any]
+        post3: dict[str, Any]
+        post1 = {'title': 'foo', 'description': 'oof', 'effort': 1.0}
+        post2 = {'title': 'bar', 'description': 'rab', 'effort': 1.1}
+        post2['new_top_step'] = 1
+        post3 = {'title': 'baz', 'description': 'zab', 'effort': 0.9}
+        post3['new_top_step'] = 1
+        self.post_process(1, post1)
+        self.post_process(2, post2)
+        self.post_process(3, post3)
+        post3['new_top_step'] = 2
+        post3['keep_step'] = 2
+        post3['steps'] = [2]
+        post3['step_2_process_id'] = 1
+        self.post_process(3, post3)
+        proc1 = self.proc_as_dict(1, post1['title'],
+                                  post1['description'], post1['effort'])
+        proc2 = self.proc_as_dict(2, post2['title'],
+                                  post2['description'], post2['effort'])
+        proc3 = self.proc_as_dict(3, post3['title'],
+                                  post3['description'], post3['effort'])
+        proc2['explicit_steps'] = [1]
+        proc3['explicit_steps'] = [2, 3]
+        step1 = self.procstep_as_dict(1, 2, 1)
+        step2 = self.procstep_as_dict(2, 3, 1)
+        step3 = self.procstep_as_dict(3, 3, 2)
+        expected = self.GET_processes_dict([proc2, proc3, proc1])
+        assert isinstance(expected['_library'], dict)
+        expected['_library']['ProcessStep'] = self.as_refs([step1, step2,
+                                                            step3])
+        self.check_json_get('/processes', expected)
+        # test other sortings
+        expected['sort_by'] = '-title'
+        expected['processes'] = self.as_id_list([proc1, proc3, proc2])
+        self.check_json_get('/processes?sort_by=-title', expected)
+        expected['sort_by'] = 'effort'
+        expected['processes'] = self.as_id_list([proc3, proc1, proc2])
+        self.check_json_get('/processes?sort_by=effort', expected)
+        expected['sort_by'] = '-effort'
+        expected['processes'] = self.as_id_list([proc2, proc1, proc3])
+        self.check_json_get('/processes?sort_by=-effort', expected)
+        expected['sort_by'] = 'steps'
+        expected['processes'] = self.as_id_list([proc1, proc2, proc3])
+        self.check_json_get('/processes?sort_by=steps', expected)
+        expected['sort_by'] = '-steps'
+        expected['processes'] = self.as_id_list([proc3, proc2, proc1])
+        self.check_json_get('/processes?sort_by=-steps', expected)
+        expected['sort_by'] = 'owners'
+        expected['processes'] = self.as_id_list([proc3, proc2, proc1])
+        self.check_json_get('/processes?sort_by=owners', expected)
+        expected['sort_by'] = '-owners'
+        expected['processes'] = self.as_id_list([proc1, proc2, proc3])
+        self.check_json_get('/processes?sort_by=-owners', expected)
+        # test pattern matching on title
+        expected = self.GET_processes_dict([proc2, proc3])
+        assert isinstance(expected['_library'], dict)
+        expected['pattern'] = 'ba'
+        expected['_library']['ProcessStep'] = self.as_refs([step1, step2,
+                                                            step3])
+        self.check_json_get('/processes?pattern=ba', expected)
+        # test pattern matching on description
+        expected['processes'] = self.as_id_list([proc1])
+        expected['_library'] = {'Process': self.as_refs([proc1])}
+        expected['pattern'] = 'of'
+        self.check_json_get('/processes?pattern=of', expected)