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=6e8842570baa4a2f1eab2293c3ab664efca2ac72;hpb=951d8ad55c0d54286f9c986257a67dfa9710fcf2;p=plomtask diff --git a/tests/todos.py b/tests/todos.py index 6e88425..d84bb70 100644 --- a/tests/todos.py +++ b/tests/todos.py @@ -1,14 +1,23 @@ """Test Todos module.""" -from tests.utils import TestCaseWithDB, TestCaseWithServer -from plomtask.todos import Todo, TodoStepsNode -from plomtask.processes import Process +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 requiring DB, but not server setup.""" +class TestsWithDB(TestCaseWithDB, TestCaseSansDB): + """Tests requiring DB, but not server setup. + + 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() @@ -20,20 +29,27 @@ class TestsWithDB(TestCaseWithDB): 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_by_id(self) -> None: - """Test creation and findability of Todos.""" - process_unsaved = Process(None) - todo = Todo(None, process_unsaved, False, self.date1) - with self.assertRaises(NotFoundException): - todo.save(self.db_conn) - process_unsaved.save(self.db_conn) - todo.save(self.db_conn) - self.assertEqual(Todo.by_id(self.db_conn, 1), todo) + def test_Todo_init(self) -> None: + """Test creation of Todo and what they default to.""" + process = Process(None) with self.assertRaises(NotFoundException): - self.assertEqual(Todo.by_id(self.db_conn, 0), todo) - with self.assertRaises(NotFoundException): - self.assertEqual(Todo.by_id(self.db_conn, 2), todo) + 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.""" @@ -43,30 +59,12 @@ class TestsWithDB(TestCaseWithDB): 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), []) - self.assertEqual(Todo.by_date(self.db_conn, 'foo'), []) + with self.assertRaises(BadFormatException): + self.assertEqual(Todo.by_date(self.db_conn, 'foo'), []) - def test_Todo_from_process(self) -> None: - """Test spawning of Todo attributes from Process.""" - assert isinstance(self.cond1.id_, int) - assert isinstance(self.cond2.id_, int) - self.proc.set_conditions(self.db_conn, [self.cond1.id_]) - todo = Todo(None, self.proc, False, self.date1) - self.assertEqual(todo.conditions, [self.cond1]) - todo.set_conditions(self.db_conn, [self.cond2.id_]) - self.assertEqual(todo.conditions, [self.cond2]) - self.assertEqual(self.proc.conditions, [self.cond1]) - self.proc.set_enables(self.db_conn, [self.cond1.id_]) - todo = Todo(None, self.proc, False, self.date1) - self.assertEqual(todo.enables, [self.cond1]) - todo.set_enables(self.db_conn, [self.cond2.id_]) - self.assertEqual(todo.enables, [self.cond2]) - self.assertEqual(self.proc.enables, [self.cond1]) - self.proc.set_disables(self.db_conn, [self.cond1.id_]) - todo = Todo(None, self.proc, False, self.date1) - self.assertEqual(todo.disables, [self.cond1]) - todo.set_disables(self.db_conn, [self.cond2.id_]) - self.assertEqual(todo.disables, [self.cond2]) - self.assertEqual(self.proc.disables, [self.cond1]) + 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.""" @@ -74,8 +72,8 @@ class TestsWithDB(TestCaseWithDB): assert isinstance(self.cond2.id_, int) todo = Todo(None, self.proc, False, self.date1) todo.save(self.db_conn) - todo.set_enables(self.db_conn, [self.cond1.id_]) - todo.set_disables(self.db_conn, [self.cond2.id_]) + 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) @@ -83,40 +81,6 @@ class TestsWithDB(TestCaseWithDB): self.assertEqual(self.cond1.is_active, True) self.assertEqual(self.cond2.is_active, False) - def test_Todo_enablers_disablers(self) -> None: - """Test Todo.enablers_for_at/disablers_for_at.""" - assert isinstance(self.cond1.id_, int) - assert isinstance(self.cond2.id_, int) - todo1 = Todo(None, self.proc, False, self.date1) - todo1.save(self.db_conn) - todo1.set_enables(self.db_conn, [self.cond1.id_]) - todo1.set_disables(self.db_conn, [self.cond2.id_]) - todo1.save(self.db_conn) - todo2 = Todo(None, self.proc, False, self.date1) - todo2.save(self.db_conn) - todo2.set_enables(self.db_conn, [self.cond2.id_]) - todo2.save(self.db_conn) - todo3 = Todo(None, self.proc, False, self.date2) - todo3.save(self.db_conn) - todo3.set_enables(self.db_conn, [self.cond2.id_]) - todo3.save(self.db_conn) - enablers = Todo.enablers_for_at(self.db_conn, self.cond1, self.date1) - self.assertEqual(enablers, [todo1]) - enablers = Todo.enablers_for_at(self.db_conn, self.cond1, self.date2) - self.assertEqual(enablers, []) - disablers = Todo.disablers_for_at(self.db_conn, self.cond1, self.date1) - self.assertEqual(disablers, []) - disablers = Todo.disablers_for_at(self.db_conn, self.cond1, self.date2) - self.assertEqual(disablers, []) - enablers = Todo.enablers_for_at(self.db_conn, self.cond2, self.date1) - self.assertEqual(enablers, [todo2]) - enablers = Todo.enablers_for_at(self.db_conn, self.cond2, self.date2) - self.assertEqual(enablers, [todo3]) - disablers = Todo.disablers_for_at(self.db_conn, self.cond2, self.date1) - self.assertEqual(disablers, [todo1]) - disablers = Todo.disablers_for_at(self.db_conn, self.cond2, self.date2) - self.assertEqual(disablers, []) - def test_Todo_children(self) -> None: """Test Todo.children relations.""" todo_1 = Todo(None, self.proc, False, self.date1) @@ -150,7 +114,8 @@ class TestsWithDB(TestCaseWithDB): todo_1.is_done = True todo_2.is_done = True todo_2.is_done = False - todo_2.set_conditions(self.db_conn, [self.cond1.id_]) + 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 @@ -158,168 +123,449 @@ class TestsWithDB(TestCaseWithDB): def test_Todo_step_tree(self) -> None: """Test self-configuration of TodoStepsNode tree for Day view.""" - assert isinstance(self.cond1.id_, int) - assert isinstance(self.cond2.id_, int) + + 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 = TodoStepsNode(todo_1, True, [], False) - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) + 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 - self.assertEqual(todo_1.get_step_tree({todo_1.id_}, set()), node_0) + 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 = TodoStepsNode(todo_2, True, [], False) + node_2 = TodoNode(todo_2, False, []) node_0.children = [node_2] node_0.seen = False - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) + 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 = TodoStepsNode(todo_3, True, [], False) + node_3 = TodoNode(todo_3, False, []) node_2.children = [node_3] - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) + 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 = TodoStepsNode(todo_3, True, [], True) + node_4 = TodoNode(todo_3, True, []) node_0.children += [node_4] - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) - # test condition shows up - todo_1.set_conditions(self.db_conn, [self.cond1.id_]) - node_5 = TodoStepsNode(self.cond1, False, [], False) - node_0.children += [node_5] - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) - # test second condition shows up - todo_2.set_conditions(self.db_conn, [self.cond2.id_]) - node_6 = TodoStepsNode(self.cond2, False, [], False) - node_2.children += [node_6] - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) - # test second condition is not hidden if fulfilled by non-sibling - todo_1.set_enables(self.db_conn, [self.cond2.id_]) - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) - # test second condition is hidden if fulfilled by sibling - todo_3.set_enables(self.db_conn, [self.cond2.id_]) - node_2.children.remove(node_6) - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) - - def test_Todo_singularity(self) -> None: - """Test pointers made for single object keep pointing to it.""" - todo = Todo(None, self.proc, False, self.date1) - todo.save(self.db_conn) - retrieved_todo = Todo.by_id(self.db_conn, 1) - todo.is_done = True - self.assertEqual(retrieved_todo.is_done, True) - retrieved_todo = Todo.by_date(self.db_conn, self.date1)[0] - retrieved_todo.is_done = False - self.assertEqual(todo.is_done, 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) + + 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_day(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, '/') - proc = Process.by_id(self.db_conn, 1) - proc2 = 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(proc.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_, proc.id_) - self.assertEqual(todo1.is_done, False) - form_data['new_todo'] = str(proc2.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_, proc2.id_) - self.assertEqual(todo1.is_done, False) - - def test_do_POST_todo(self) -> None: - """Test POST /todo.""" - def post_and_reload(form_data: dict[str, object], - status: int = 302) -> Todo: - self.check_post(form_data, '/todo?id=1', status, '/') - self.db_conn.cached_todos = {} - return Todo.by_date(self.db_conn, '2024-01-01')[0] - # test minimum - form_data = {'title': '', 'description': '', 'effort': 1} - self.check_post(form_data, '/process', 302) - form_data = {'comment': '', 'new_todo': 1} - self.check_post(form_data, '/day?date=2024-01-01', 302) - # test posting to bad URLs - form_data = {} - self.check_post(form_data, '/todo=', 404) - self.check_post(form_data, '/todo?id=', 400) - self.check_post(form_data, '/todo?id=FOO', 400) - self.check_post(form_data, '/todo?id=0', 404) - # test posting naked entity - todo1 = post_and_reload(form_data) - self.assertEqual(todo1.children, []) - self.assertEqual(todo1.parents, []) - self.assertEqual(todo1.is_done, False) - # test posting doneness - form_data = {'done': ''} - todo1 = post_and_reload(form_data) - self.assertEqual(todo1.is_done, True) - # test implicitly posting non-doneness - form_data = {} - todo1 = post_and_reload(form_data) - self.assertEqual(todo1.is_done, False) - # test malformed adoptions - form_data = {'adopt': 'foo'} - self.check_post(form_data, '/todo?id=1', 400) - form_data = {'adopt': 1} - self.check_post(form_data, '/todo?id=1', 400) - form_data = {'adopt': 2} - self.check_post(form_data, '/todo?id=1', 404) - # test posting second todo of same process - form_data = {'comment': '', 'new_todo': 1} - self.check_post(form_data, '/day?date=2024-01-01', 302) - # test todo 1 adopting todo 2 - form_data = {'adopt': 2} - todo1 = post_and_reload(form_data) - todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1] - self.assertEqual(todo1.children, [todo2]) - self.assertEqual(todo1.parents, []) - self.assertEqual(todo2.children, []) - self.assertEqual(todo2.parents, [todo1]) - # test todo1 cannot be set done with todo2 not done yet - form_data = {'done': '', 'adopt': 2} - todo1 = post_and_reload(form_data, 400) - self.assertEqual(todo1.is_done, False) - # test todo1 un-adopting todo 2 by just not sending an adopt - form_data = {} - todo1 = post_and_reload(form_data, 302) - todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1] - self.assertEqual(todo1.children, []) - self.assertEqual(todo1.parents, []) - self.assertEqual(todo2.children, []) - self.assertEqual(todo2.parents, []) - - def test_do_GET_todo(self) -> None: + 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.""" - form_data = {'title': '', 'description': '', 'effort': 1} - self.check_post(form_data, '/process?id=', 302, '/') - form_data = {'comment': '', 'new_todo': 1} - self.check_post(form_data, '/day?date=2024-01-01', 302, '/') - self.check_get('/todo', 400) - self.check_get('/todo?id=', 400) + # 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=1', 200) + 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')