X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/move_up?a=blobdiff_plain;ds=sidebyside;f=tests%2Ftodos.py;h=dd57ee4c0c28cfc73d3d9c08dd6c18ab2dd7cd7b;hb=HEAD;hp=93b34d197f33aebbd5b096619c3804ca0b8f75ce;hpb=a4ca74f81ae42abe27cf6dbab7ef18c850db72c2;p=plomtask diff --git a/tests/todos.py b/tests/todos.py index 93b34d1..d84bb70 100644 --- a/tests/todos.py +++ b/tests/todos.py @@ -1,60 +1,571 @@ """Test Todos module.""" -from tests.utils import TestCaseWithDB, TestCaseWithServer -from plomtask.todos import Todo -from plomtask.days import Day -from plomtask.processes import Process -from plomtask.exceptions import NotFoundException +from typing import Any +from tests.utils import (TestCaseSansDB, TestCaseWithDB, TestCaseWithServer, + Expected) +from plomtask.todos import Todo, TodoNode +from plomtask.processes import Process, ProcessStep +from plomtask.conditions import Condition +from plomtask.exceptions import (NotFoundException, BadFormatException, + HandledException) -class TestsWithDB(TestCaseWithDB): - """Tests not requiring DB setup.""" +class TestsWithDB(TestCaseWithDB, TestCaseSansDB): + """Tests requiring DB, but not server setup. - def test_Todo_by_date(self) -> None: - """Test creation and findability of Todos.""" - day1 = Day('2024-01-01') - day2 = Day('2024-01-02') - process1 = Process(None) - todo1 = Todo(None, process1, False, day1) - with self.assertRaises(NotFoundException): - todo1.save(self.db_conn) - process1.save_without_steps(self.db_conn) - todo1.save(self.db_conn) - todo2 = Todo(None, process1, False, day1) - todo2.save(self.db_conn) + NB: We subclass TestCaseSansDB too, to run any tests there that due to any + Todo requiring a _saved_ Process wouldn't run without a DB. + """ + checked_class = Todo + default_init_kwargs = {'process': None, 'is_done': False, + 'date': '2024-01-01'} + + def setUp(self) -> None: + super().setUp() + self.date1 = '2024-01-01' + self.date2 = '2024-01-02' + self.proc = Process(None) + self.proc.save(self.db_conn) + self.cond1 = Condition(None) + self.cond1.save(self.db_conn) + self.cond2 = Condition(None) + self.cond2.save(self.db_conn) + self.default_init_kwargs['process'] = self.proc + + def test_Todo_init(self) -> None: + """Test creation of Todo and what they default to.""" + process = Process(None) with self.assertRaises(NotFoundException): - Todo.by_date(self.db_conn, day1.date), - day1.save(self.db_conn) - day2.save(self.db_conn) - self.assertEqual(Todo.by_date(self.db_conn, day1.date), [todo1, todo2]) - self.assertEqual(Todo.by_date(self.db_conn, day2.date), []) - self.assertEqual(Todo.by_date(self.db_conn, 'foo'), []) + Todo(None, process, False, self.date1) + process.save(self.db_conn) + assert isinstance(self.cond1.id_, int) + assert isinstance(self.cond2.id_, int) + process.set_condition_relations(self.db_conn, + [self.cond1.id_, self.cond2.id_], [], + [self.cond1.id_], [self.cond2.id_]) + todo_no_id = Todo(None, process, False, self.date1) + self.assertEqual(todo_no_id.conditions, [self.cond1, self.cond2]) + self.assertEqual(todo_no_id.enables, [self.cond1]) + self.assertEqual(todo_no_id.disables, [self.cond2]) + todo_yes_id = Todo(5, process, False, self.date1) + self.assertEqual(todo_yes_id.conditions, []) + self.assertEqual(todo_yes_id.enables, []) + self.assertEqual(todo_yes_id.disables, []) + + def test_Todo_by_date(self) -> None: + """Test findability of Todos by date.""" + t1 = Todo(None, self.proc, False, self.date1) + t1.save(self.db_conn) + t2 = Todo(None, self.proc, False, self.date1) + t2.save(self.db_conn) + self.assertEqual(Todo.by_date(self.db_conn, self.date1), [t1, t2]) + self.assertEqual(Todo.by_date(self.db_conn, self.date2), []) + with self.assertRaises(BadFormatException): + self.assertEqual(Todo.by_date(self.db_conn, 'foo'), []) + + def test_Todo_by_date_range_with_limits(self) -> None: + """Test .by_date_range_with_limits.""" + self.check_by_date_range_with_limits('day') + + def test_Todo_on_conditions(self) -> None: + """Test effect of Todos on Conditions.""" + assert isinstance(self.cond1.id_, int) + assert isinstance(self.cond2.id_, int) + todo = Todo(None, self.proc, False, self.date1) + todo.save(self.db_conn) + todo.set_condition_relations(self.db_conn, [], [], + [self.cond1.id_], [self.cond2.id_]) + todo.is_done = True + self.assertEqual(self.cond1.is_active, True) + self.assertEqual(self.cond2.is_active, False) + todo.is_done = False + self.assertEqual(self.cond1.is_active, True) + self.assertEqual(self.cond2.is_active, False) + + def test_Todo_children(self) -> None: + """Test Todo.children relations.""" + todo_1 = Todo(None, self.proc, False, self.date1) + todo_2 = Todo(None, self.proc, False, self.date1) + todo_2.save(self.db_conn) + with self.assertRaises(HandledException): + todo_1.add_child(todo_2) + todo_1.save(self.db_conn) + todo_3 = Todo(None, self.proc, False, self.date1) + with self.assertRaises(HandledException): + todo_1.add_child(todo_3) + todo_3.save(self.db_conn) + todo_1.add_child(todo_3) + todo_1.save(self.db_conn) + assert isinstance(todo_1.id_, int) + todo_retrieved = Todo.by_id(self.db_conn, todo_1.id_) + self.assertEqual(todo_retrieved.children, [todo_3]) + with self.assertRaises(BadFormatException): + todo_3.add_child(todo_1) + + def test_Todo_conditioning(self) -> None: + """Test Todo.doability conditions.""" + assert isinstance(self.cond1.id_, int) + todo_1 = Todo(None, self.proc, False, self.date1) + todo_1.save(self.db_conn) + todo_2 = Todo(None, self.proc, False, self.date1) + todo_2.save(self.db_conn) + todo_2.add_child(todo_1) + with self.assertRaises(BadFormatException): + todo_2.is_done = True + todo_1.is_done = True + todo_2.is_done = True + todo_2.is_done = False + todo_2.set_condition_relations( + self.db_conn, [self.cond1.id_], [], [], []) + with self.assertRaises(BadFormatException): + todo_2.is_done = True + self.cond1.is_active = True + todo_2.is_done = True + + def test_Todo_step_tree(self) -> None: + """Test self-configuration of TodoStepsNode tree for Day view.""" + + def todo_node_as_dict(node: TodoNode) -> dict[str, object]: + return {'todo': node.todo.id_, 'seen': node.seen, + 'children': [todo_node_as_dict(c) for c in node.children]} + + todo_1 = Todo(None, self.proc, False, self.date1) + todo_1.save(self.db_conn) + assert isinstance(todo_1.id_, int) + # test minimum + node_0 = TodoNode(todo_1, False, []) + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree(set())) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) + # test non_emtpy seen_todo does something + node_0.seen = True + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree({todo_1.id_})) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) + # test child shows up + todo_2 = Todo(None, self.proc, False, self.date1) + todo_2.save(self.db_conn) + assert isinstance(todo_2.id_, int) + todo_1.add_child(todo_2) + node_2 = TodoNode(todo_2, False, []) + node_0.children = [node_2] + node_0.seen = False + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree(set())) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) + # test child shows up with child + todo_3 = Todo(None, self.proc, False, self.date1) + todo_3.save(self.db_conn) + assert isinstance(todo_3.id_, int) + todo_2.add_child(todo_3) + node_3 = TodoNode(todo_3, False, []) + node_2.children = [node_3] + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree(set())) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) + # test same todo can be child-ed multiple times at different locations + todo_1.add_child(todo_3) + node_4 = TodoNode(todo_3, True, []) + node_0.children += [node_4] + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree(set())) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) + + def test_Todo_ensure_children(self) -> None: + """Test parenthood guarantees of Todo.ensure_children.""" + assert isinstance(self.proc.id_, int) + proc2 = Process(None) + proc2.save(self.db_conn) + assert isinstance(proc2.id_, int) + proc3 = Process(None) + proc3.save(self.db_conn) + assert isinstance(proc3.id_, int) + proc4 = Process(None) + proc4.save(self.db_conn) + assert isinstance(proc4.id_, int) + # make proc4 step of proc3 + step = ProcessStep(None, proc3.id_, proc4.id_, None) + proc3.set_steps(self.db_conn, [step]) + # give proc2 three steps; 2× proc1, 1× proc3 + step1 = ProcessStep(None, proc2.id_, self.proc.id_, None) + step2 = ProcessStep(None, proc2.id_, self.proc.id_, None) + step3 = ProcessStep(None, proc2.id_, proc3.id_, None) + proc2.set_steps(self.db_conn, [step1, step2, step3]) + # test mere creation does nothing + todo_ignore = Todo(None, proc2, False, self.date1) + todo_ignore.save(self.db_conn) + self.assertEqual(todo_ignore.children, []) + # test create_with_children on step-less does nothing + todo_1 = Todo(None, self.proc, False, self.date1) + todo_1.save(self.db_conn) + todo_1.ensure_children(self.db_conn) + self.assertEqual(todo_1.children, []) + self.assertEqual(len(Todo.all(self.db_conn)), 2) + # test create_with_children adopts and creates, and down tree too + todo_2 = Todo(None, proc2, False, self.date1) + todo_2.save(self.db_conn) + todo_2.ensure_children(self.db_conn) + self.assertEqual(3, len(todo_2.children)) + self.assertEqual(todo_1, todo_2.children[0]) + self.assertEqual(self.proc, todo_2.children[2].process) + self.assertEqual(proc3, todo_2.children[1].process) + todo_3 = todo_2.children[1] + self.assertEqual(len(todo_3.children), 1) + self.assertEqual(todo_3.children[0].process, proc4) + + +class ExpectedGetTodo(Expected): + """Builder of expectations for GET /todo.""" + + def __init__(self, + todo_id: int, + *args: Any, **kwargs: Any) -> None: + self._fields = {'todo': todo_id, + 'steps_todo_to_process': []} + super().__init__(*args, **kwargs) + + def recalc(self) -> None: + """Update internal dictionary by subclass-specific rules.""" + + def walk_steps(step: dict[str, Any]) -> None: + if not step['todo']: + proc_id = step['process'] + cands = self.as_ids( + [t for t in todos if proc_id == t['process_id'] + and t['id'] in self._fields['todo_candidates']]) + self._fields['adoption_candidates_for'][str(proc_id)] = cands + for child in step['children']: + walk_steps(child) + + super().recalc() + self.lib_wipe('Day') + todos = self.lib_all('Todo') + procs = self.lib_all('Process') + conds = self.lib_all('Condition') + self._fields['todo_candidates'] = self.as_ids( + [t for t in todos if t['id'] != self._fields['todo']]) + self._fields['process_candidates'] = self.as_ids(procs) + self._fields['condition_candidates'] = self.as_ids(conds) + self._fields['adoption_candidates_for'] = {} + for step in self._fields['steps_todo_to_process']: + walk_steps(step) + + @staticmethod + def step_as_dict(node_id: int, + children: list[dict[str, object]], + process: int | None = None, + todo: int | None = None, + fillable: bool = False, + ) -> dict[str, object]: + """Return JSON of TodoOrProcStepsNode to expect.""" + return {'node_id': node_id, + 'children': children, + 'process': process, + 'fillable': fillable, + 'todo': todo} class TestsWithServer(TestCaseWithServer): """Tests against our HTTP server/handler (and database).""" - def test_do_POST_todo(self) -> None: - """Test Todo posting of POST /day.""" - form_data = {'title': '', 'description': '', 'effort': 1} - self.check_post(form_data, '/process?id=', 302, '/') - self.check_post(form_data, '/process?id=', 302, '/') - process1 = Process.by_id(self.db_conn, 1) - process2 = Process.by_id(self.db_conn, 2) - form_data = {'comment': ''} - self.check_post(form_data, '/day?date=2024-01-01', 302, '/') - self.assertEqual(Todo.by_date(self.db_conn, '2024-01-01'), []) - form_data['new_todo'] = str(process1.id_) - self.check_post(form_data, '/day?date=2024-01-01', 302, '/') - todos = Todo.by_date(self.db_conn, '2024-01-01') - self.assertEqual(1, len(todos)) - todo1 = todos[0] - self.assertEqual(todo1.id_, 1) - self.assertEqual(todo1.process.id_, process1.id_) - self.assertEqual(todo1.is_done, False) - form_data['new_todo'] = str(process2.id_) - self.check_post(form_data, '/day?date=2024-01-01', 302, '/') - todos = Todo.by_date(self.db_conn, '2024-01-01') - todo1 = todos[1] - self.assertEqual(todo1.id_, 2) - self.assertEqual(todo1.process.id_, process2.id_) - self.assertEqual(todo1.is_done, False) + def _post_exp_todo( + self, id_: int, payload: dict[str, Any], exp: Expected) -> None: + self.check_post(payload, f'/todo?id={id_}') + exp.set_todo_from_post(id_, payload) + + def test_basic_fail_POST_todo(self) -> None: + """Test basic malformed/illegal POST /todo requests.""" + self.post_exp_process([], {}, 1) + # test we cannot just POST into non-existing Todo + self.check_post({}, '/todo', 404) + self.check_post({}, '/todo?id=FOO', 400) + self.check_post({}, '/todo?id=0', 404) + self.check_post({}, '/todo?id=1', 404) + # test malformed values on existing Todo + self.post_exp_day([], {'new_todo': [1]}) + for name in [ + 'adopt', 'effort', 'make_full', 'make_empty', 'step_filler', + 'conditions', 'disables', 'blockers', 'enables']: + self.check_post({name: 'x'}, '/todo?id=1', 400, '/todo') + for prefix in ['make_empty_', 'make_full_']: + for suffix in ['', 'x', '1.1']: + self.check_post({'step_filler': f'{prefix}{suffix}'}, + '/todo?id=1', 400, '/todo') + + def test_basic_POST_todo(self) -> None: + """Test basic POST /todo manipulations.""" + exp = ExpectedGetTodo(1) + self.post_exp_process([exp], {'calendarize': 0}, 1) + self.post_exp_day([exp], {'new_todo': [1]}) + # test posting naked entity at first changes nothing + self.check_json_get('/todo?id=1', exp) + self.check_post({}, '/todo?id=1') + self.check_json_get('/todo?id=1', exp) + # test posting doneness, comment, calendarization, effort + todo_post = {'is_done': 1, 'calendarize': 1, + 'comment': 'foo', 'effort': 2.3} + self._post_exp_todo(1, todo_post, exp) + self.check_json_get('/todo?id=1', exp) + # test implicitly un-setting (only) comment by empty post + self.check_post({}, '/todo?id=1') + exp.lib_get('Todo', 1)['comment'] = '' + self.check_json_get('/todo?id=1', exp) + # test effort post can be explicitly unset by "effort":"" post + self.check_post({'effort': ''}, '/todo?id=1') + exp.lib_get('Todo', 1)['effort'] = None + self.check_json_get('/todo?id=1', exp) + # test Condition posts + c1_post = {'title': 'foo', 'description': 'oof', 'is_active': 0} + c2_post = {'title': 'bar', 'description': 'rab', 'is_active': 1} + self.post_exp_cond([exp], 1, c1_post, '?id=1', '?id=1') + self.post_exp_cond([exp], 2, c2_post, '?id=2', '?id=2') + self.check_json_get('/todo?id=1', exp) + todo_post = {'conditions': [1], 'disables': [1], + 'blockers': [2], 'enables': [2]} + self._post_exp_todo(1, todo_post, exp) + self.check_json_get('/todo?id=1', exp) + + def test_POST_todo_deletion(self) -> None: + """Test deletions via POST /todo.""" + exp = ExpectedGetTodo(1) + self.post_exp_process([exp], {}, 1) + # test failure of deletion on non-existing Todo + self.check_post({'delete': ''}, '/todo?id=2', 404, '/') + # test deletion of existing Todo + self.post_exp_day([exp], {'new_todo': [1]}) + self.check_post({'delete': ''}, '/todo?id=1', 302, '/') + self.check_get('/todo?id=1', 404) + exp.lib_del('Todo', 1) + # test deletion of adopted Todo + self.post_exp_day([exp], {'new_todo': [1]}) + self.post_exp_day([exp], {'new_todo': [1]}) + self.check_post({'adopt': 2}, '/todo?id=1') + self.check_post({'delete': ''}, '/todo?id=2', 302, '/') + exp.lib_del('Todo', 2) + self.check_get('/todo?id=2', 404) + self.check_json_get('/todo?id=1', exp) + # test deletion of adopting Todo + self.post_exp_day([exp], {'new_todo': [1]}) + self.check_post({'adopt': 2}, '/todo?id=1') + self.check_post({'delete': ''}, '/todo?id=1', 302, '/') + exp.set('todo', 2) + exp.lib_del('Todo', 1) + self.check_json_get('/todo?id=2', exp) + # test cannot delete Todo with comment or effort + self.check_post({'comment': 'foo'}, '/todo?id=2') + self.check_post({'delete': ''}, '/todo?id=2', 500, '/') + self.check_post({'effort': 5}, '/todo?id=2') + self.check_post({'delete': ''}, '/todo?id=2', 500, '/') + # test deletion via effort < 0, but only if deletable + self.check_post({'effort': -1, 'comment': 'foo'}, '/todo?id=2') + self.check_post({}, '/todo?id=2') + self.check_get('/todo?id=2', 404) + + def test_POST_todo_adoption(self) -> None: + """Test adoption via POST /todo with "adopt".""" + # post two Todos to Day, have first adopt second + exp = ExpectedGetTodo(1) + self.post_exp_process([exp], {}, 1) + self.post_exp_day([exp], {'new_todo': [1]}) + self.post_exp_day([exp], {'new_todo': [1]}) + self._post_exp_todo(1, {'adopt': 2}, exp) + exp.set('steps_todo_to_process', [exp.step_as_dict(1, [], todo=2)]) + self.check_json_get('/todo?id=1', exp) + # test Todo un-adopting by just not sending an adopt + self._post_exp_todo(1, {}, exp) + exp.set('steps_todo_to_process', []) + self.check_json_get('/todo?id=1', exp) + # test fail on trying to adopt non-existing Todo + self.check_post({'adopt': 3}, '/todo?id=1', 404) + # test cannot self-adopt + self.check_post({'adopt': 1}, '/todo?id=1', 400) + # test cannot do 1-step circular adoption + self._post_exp_todo(2, {'adopt': 1}, exp) + self.check_post({'adopt': 2}, '/todo?id=1', 400) + # test cannot do 2-step circular adoption + self.post_exp_day([exp], {'new_todo': [1]}) + self._post_exp_todo(3, {'adopt': 2}, exp) + self.check_post({'adopt': 3}, '/todo?id=1', 400) + # test can adopt Todo into ProcessStep chain via its Process (with key + # 'step_filler' equivalent to single-element 'adopt' if intable) + self.post_exp_process([exp], {}, 2) + self.post_exp_process([exp], {}, 3) + self.post_exp_process([exp], {'new_top_step': [2, 3]}, 1) + exp.lib_set('ProcessStep', [exp.procstep_as_dict(1, 1, 2), + exp.procstep_as_dict(2, 1, 3)]) + step1_proc2 = exp.step_as_dict(1, [], 2, None, True) + step2_proc3 = exp.step_as_dict(2, [], 3, None, True) + exp.set('steps_todo_to_process', [step1_proc2, step2_proc3]) + self.post_exp_day([exp], {'new_todo': [2]}) + self.post_exp_day([exp], {'new_todo': [3]}) + self.check_json_get('/todo?id=1', exp) + self._post_exp_todo(1, {'step_filler': 5, 'adopt': [4]}, exp) + step1_proc2 = exp.step_as_dict(1, [], 2, 4, True) + step2_proc3 = exp.step_as_dict(2, [], 3, 5, True) + exp.set('steps_todo_to_process', [step1_proc2, step2_proc3]) + self.check_json_get('/todo?id=1', exp) + # test 'ignore' values for 'step_filler' are ignored, and intable + # 'step_filler' values are interchangeable with those of 'adopt' + todo_post = {'adopt': 5, 'step_filler': ['ignore', 4]} + self.check_post(todo_post, '/todo?id=1') + self.check_json_get('/todo?id=1', exp) + # test cannot adopt into non-top-level elements of chain, instead + # creating new top-level steps when adopting of respective Process + self.post_exp_process([exp], {}, 4) + self.post_exp_process([exp], {'new_top_step': 4, 'step_of': [1]}, 3) + exp.lib_set('ProcessStep', [exp.procstep_as_dict(3, 3, 4)]) + step3_proc4 = exp.step_as_dict(3, [], 4, None, True) + step2_proc3 = exp.step_as_dict(2, [step3_proc4], 3, 5, True) + exp.set('steps_todo_to_process', [step1_proc2, step2_proc3]) + self.post_exp_day([exp], {'new_todo': [4]}) + self._post_exp_todo(1, {'adopt': [4, 5, 6]}, exp) + step4_todo6 = exp.step_as_dict(4, [], None, 6, False) + exp.set('steps_todo_to_process', [step1_proc2, step2_proc3, + step4_todo6]) + self.check_json_get('/todo?id=1', exp) + + def test_POST_todo_make_full(self) -> None: + """Test creation and adoption via POST /todo with "make_full".""" + # create chain of Processes + exp = ExpectedGetTodo(1) + self.post_exp_process([exp], {}, 1) + for i in range(1, 4): + self.post_exp_process([exp], {'new_top_step': i}, i+1) + exp.lib_set('ProcessStep', [exp.procstep_as_dict(1, 2, 1), + exp.procstep_as_dict(2, 3, 2), + exp.procstep_as_dict(3, 4, 3)]) + step3_proc1 = exp.step_as_dict(3, [], 1, None, False) + step2_proc2 = exp.step_as_dict(2, [step3_proc1], 2, None, False) + step1_proc3 = exp.step_as_dict(1, [step2_proc2], 3, None, True) + exp.set('steps_todo_to_process', [step1_proc3]) + # post (childless) Todo of chain end, then make_full on next in line + self.post_exp_day([exp], {'new_todo': [4]}) + self.check_post({'step_filler': 'make_full_3'}, '/todo?id=1') + exp.set_todo_from_post(4, {'process_id': 1}) + exp.set_todo_from_post(3, {'process_id': 2, 'children': [4]}) + exp.set_todo_from_post(2, {'process_id': 3, 'children': [3]}) + exp.set_todo_from_post(1, {'process_id': 4, 'children': [2]}) + step3_proc1 = exp.step_as_dict(3, [], 1, 4, True) + step2_proc2 = exp.step_as_dict(2, [step3_proc1], 2, 3, True) + step1_proc3 = exp.step_as_dict(1, [step2_proc2], 3, 2, True) + exp.set('steps_todo_to_process', [step1_proc3]) + self.check_json_get('/todo?id=1', exp) + # make new chain next to expected, find steps_todo_to_process extended, + # expect existing Todo demanded by new chain be adopted into new chain + self.check_post({'make_full': 2, 'adopt': [2]}, '/todo?id=1') + exp.set_todo_from_post(5, {'process_id': 2, 'children': [4]}) + exp.set_todo_from_post(1, {'process_id': 4, 'children': [2, 5]}) + step5_todo4 = exp.step_as_dict(5, [], None, 4) + step4_todo5 = exp.step_as_dict(4, [step5_todo4], None, 5) + exp.set('steps_todo_to_process', [step1_proc3, step4_todo5]) + self.check_json_get('/todo?id=1', exp) + # fail on trying to call make_full on non-existing Process + self.check_post({'make_full': 5}, '/todo?id=1', 404) + + def test_POST_todo_make_empty(self) -> None: + """Test creation and adoption via POST /todo with "make_empty".""" + # create chain of Processes + exp = ExpectedGetTodo(1) + self.post_exp_process([exp], {}, 1) + for i in range(1, 4): + self.post_exp_process([exp], {'new_top_step': i}, i+1) + exp.lib_set('ProcessStep', [exp.procstep_as_dict(1, 2, 1), + exp.procstep_as_dict(2, 3, 2), + exp.procstep_as_dict(3, 4, 3)]) + # post (childless) Todo of chain end, then make empty on next in line + self.post_exp_day([exp], {'new_todo': [4]}) + step3_proc1 = exp.step_as_dict(3, [], 1) + step2_proc2 = exp.step_as_dict(2, [step3_proc1], 2) + step1_proc3 = exp.step_as_dict(1, [step2_proc2], 3, None, True) + exp.set('steps_todo_to_process', [step1_proc3]) + self.check_json_get('/todo?id=1', exp) + self.check_post({'step_filler': 'make_empty_3'}, '/todo?id=1') + exp.set_todo_from_post(2, {'process_id': 3}) + exp.set_todo_from_post(1, {'process_id': 4, 'children': [2]}) + step2_proc2 = exp.step_as_dict(2, [step3_proc1], 2, None, True) + step1_proc3 = exp.step_as_dict(1, [step2_proc2], 3, 2, True) + exp.set('steps_todo_to_process', [step1_proc3]) + self.check_json_get('/todo?id=1', exp) + # make new top-level Todo without chain implied by its Process + self.check_post({'make_empty': 2, 'adopt': [2]}, '/todo?id=1') + exp.set_todo_from_post(3, {'process_id': 2}) + exp.set_todo_from_post(1, {'process_id': 4, 'children': [2, 3]}) + step4_todo3 = exp.step_as_dict(4, [], None, 3) + exp.set('steps_todo_to_process', [step1_proc3, step4_todo3]) + self.check_json_get('/todo?id=1', exp) + # fail on trying to call make_empty on non-existing Process + self.check_post({'make_full': 5}, '/todo?id=1', 404) + + def test_GET_todo(self) -> None: + """Test GET /todo response codes.""" + # test malformed or illegal parameter values + self.check_get('/todo', 404) + self.check_get('/todo?id=', 404) + self.check_get('/todo?id=foo', 400) + self.check_get('/todo?id=0', 404) + self.check_get('/todo?id=2', 404) + # test all existing Processes are shown as available + exp = ExpectedGetTodo(1) + self.post_exp_process([exp], {}, 1) + self.post_exp_day([exp], {'new_todo': [1]}) + self.post_exp_process([exp], {}, 2) + self.check_json_get('/todo?id=1', exp) + # test chain of Processes shown as potential step nodes + self.post_exp_process([exp], {}, 3) + self.post_exp_process([exp], {}, 4) + self.post_exp_process([exp], {'new_top_step': 2}, 1) + self.post_exp_process([exp], {'new_top_step': 3, 'step_of': [1]}, 2) + self.post_exp_process([exp], {'new_top_step': 4, 'step_of': [2]}, 3) + exp.lib_set('ProcessStep', [exp.procstep_as_dict(1, 1, 2, None), + exp.procstep_as_dict(2, 2, 3, None), + exp.procstep_as_dict(3, 3, 4, None)]) + step3_proc4 = exp.step_as_dict(3, [], 4) + step2_proc3 = exp.step_as_dict(2, [step3_proc4], 3) + step1_proc2 = exp.step_as_dict(1, [step2_proc3], 2, fillable=True) + exp.set('steps_todo_to_process', [step1_proc2]) + self.check_json_get('/todo?id=1', exp) + # test display of parallel chains + proc_steps_post = {'new_top_step': 4, 'kept_steps': [1, 3]} + self.post_exp_process([], proc_steps_post, 1) + step4_proc4 = exp.step_as_dict(4, [], 4, fillable=True) + exp.lib_set('ProcessStep', [exp.procstep_as_dict(4, 1, 4, None)]) + exp.set('steps_todo_to_process', [step1_proc2, step4_proc4]) + self.check_json_get('/todo?id=1', exp) + + def test_POST_todo_doneness_relations(self) -> None: + """Test Todo.is_done Condition, adoption relations for /todo POSTs.""" + self.post_exp_process([], {}, 1) + # test Todo with adoptee can only be set done if adoptee is done too + self.post_exp_day([], {'new_todo': [1]}) + self.post_exp_day([], {'new_todo': [1]}) + self.check_post({'adopt': 2, 'is_done': 1}, '/todo?id=1', 400) + self.check_post({'is_done': 1}, '/todo?id=2') + self.check_post({'adopt': 2, 'is_done': 1}, '/todo?id=1', 302) + # test Todo cannot be set undone with adopted Todo not done yet + self.check_post({'is_done': 0}, '/todo?id=2') + self.check_post({'adopt': 2, 'is_done': 0}, '/todo?id=1', 400) + # test unadoption relieves block + self.check_post({'is_done': 0}, '/todo?id=1', 302) + # test Condition being set or unset can block doneness setting + c1_post = {'title': '', 'description': '', 'is_active': 0} + c2_post = {'title': '', 'description': '', 'is_active': 1} + self.check_post(c1_post, '/condition', redir='/condition?id=1') + self.check_post(c2_post, '/condition', redir='/condition?id=2') + self.check_post({'conditions': [1], 'is_done': 1}, '/todo?id=1', 400) + self.check_post({'is_done': 1}, '/todo?id=1', 302) + self.check_post({'is_done': 0}, '/todo?id=1', 302) + self.check_post({'blockers': [2], 'is_done': 1}, '/todo?id=1', 400) + self.check_post({'is_done': 1}, '/todo?id=1', 302) + # test setting Todo doneness can set/un-set Conditions, but only on + # doneness change, not by mere passive state + self.check_post({'is_done': 0}, '/todo?id=2', 302) + self.check_post({'enables': [1], 'is_done': 1}, '/todo?id=1') + self.check_post({'conditions': [1], 'is_done': 1}, '/todo?id=2', 400) + self.check_post({'enables': [1], 'is_done': 0}, '/todo?id=1') + self.check_post({'enables': [1], 'is_done': 1}, '/todo?id=1') + self.check_post({'conditions': [1], 'is_done': 1}, '/todo?id=2') + self.check_post({'blockers': [1], 'is_done': 0}, '/todo?id=2', 400) + self.check_post({'disables': [1], 'is_done': 1}, '/todo?id=1') + self.check_post({'blockers': [1], 'is_done': 0}, '/todo?id=2', 400) + self.check_post({'disables': [1]}, '/todo?id=1') + self.check_post({'disables': [1], 'is_done': 1}, '/todo?id=1') + self.check_post({'blockers': [1]}, '/todo?id=2')