From caf992b4dbeb534d0da59f7a76b82bab30a2d0c1 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 13 Aug 2024 06:24:05 +0200 Subject: [PATCH] Extend and refactor tests. --- tests/conditions.py | 9 ++--- tests/days.py | 77 +++++++++++++++++++++----------------- tests/processes.py | 5 +-- tests/todos.py | 90 +++++++++++++++++++++++++-------------------- tests/utils.py | 11 +++++- 5 files changed, 107 insertions(+), 85 deletions(-) diff --git a/tests/conditions.py b/tests/conditions.py index 58fa18b..3b64959 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -77,13 +77,10 @@ class TestsWithServer(TestCaseWithServer): def test_fail_POST_condition(self) -> None: """Test malformed/illegal POST /condition requests.""" # check incomplete POST payloads - url = '/condition' - self.check_post({}, url, 400) - self.check_post({'title': ''}, url, 400) - self.check_post({'description': ''}, url, 400) + valid_payload = {'title': '', 'description': ''} + self.check_minimal_inputs('/condition', valid_payload) # check valid POST payload on bad paths - valid_payload = {'title': '', 'description': '', 'is_active': 0} - self.check_post(valid_payload, f'{url}?id=foo', 400) + self.check_post(valid_payload, '/condition?id=foo', 400) def test_POST_condition(self) -> None: """Test (valid) POST /condition and its effect on GET /condition[s].""" diff --git a/tests/days.py b/tests/days.py index f79284c..c195237 100644 --- a/tests/days.py +++ b/tests/days.py @@ -56,7 +56,9 @@ class TestsWithDB(TestCaseWithDB): def test_Day_with_filled_gaps(self) -> None: """Test .with_filled_gaps.""" - def test(range_indexes: tuple[int, int], indexes_to_provide: list[int] + def expect_within_full_range_as_commented( + range_indexes: tuple[int, int], + indexes_to_provide: list[int] ) -> None: start_i, end_i = range_indexes days_provided = [] @@ -78,32 +80,24 @@ class TestsWithDB(TestCaseWithDB): days_with_comment = [Day(date, comment=date[-1:]) for date in dates] days_sans_comment = [Day(date, comment='') for date in dates] # check provided Days recognizable in (full-range) interval - test((0, 8), [0, 4, 8]) + expect_within_full_range_as_commented((0, 8), [0, 4, 8]) # check limited range, but limiting Days provided - test((2, 6), [2, 5, 6]) + expect_within_full_range_as_commented((2, 6), [2, 5, 6]) # check Days within range but beyond provided Days also filled in - test((1, 7), [2, 5]) + expect_within_full_range_as_commented((1, 7), [2, 5]) # check provided Days beyond range ignored - test((3, 5), [1, 2, 4, 6, 7]) + expect_within_full_range_as_commented((3, 5), [1, 2, 4, 6, 7]) # check inversion of start_date and end_date returns empty list - test((5, 3), [2, 4, 6]) + expect_within_full_range_as_commented((5, 3), [2, 4, 6]) # check empty provision still creates filler elements in interval - test((3, 5), []) + expect_within_full_range_as_commented((3, 5), []) # check single-element selection creating only filler beyond provided - test((1, 1), [2, 4, 6]) + expect_within_full_range_as_commented((1, 1), [2, 4, 6]) # check (un-saved) filler Days don't show up in cache or DB - # dates = [f'2024-02-0{n}' for n in range(1, 6)] day = Day(dates[3]) day.save(self.db_conn) self.checked_class.with_filled_gaps([day], dates[0], dates[-1]) self.check_identity_with_cache_and_db([day]) - # check 'today', 'yesterday', 'tomorrow' are interpreted - yesterday = Day('yesterday') - tomorrow = Day('tomorrow') - today = Day('today') - result = self.checked_class.with_filled_gaps([today], 'yesterday', - 'tomorrow') - self.assertEqual(result, [yesterday, today, tomorrow]) class ExpectedGetCalendar(Expected): @@ -167,10 +161,9 @@ class TestsWithServer(TestCaseWithServer): self.check_get_defaults('/day', '2024-01-01', 'date') self.check_get('/day?date=2024-02-30', 400) # check undefined day - date = _testing_date_in_n_days(0) - exp = ExpectedGetDay(date) + exp = ExpectedGetDay(_testing_date_in_n_days(0)) self.check_json_get('/day', exp) - # check defined day, with and without make_type parameter + # check defined day with make_type parameter date = '2024-01-01' exp = ExpectedGetDay(date) exp.set('make_type', 'bar') @@ -185,22 +178,19 @@ class TestsWithServer(TestCaseWithServer): """Test malformed/illegal POST /day requests.""" # check payloads lacking minimum expecteds url = '/day?date=2024-01-01' - self.check_post({}, url, 400) - self.check_post({'day_comment': ''}, url, 400) - self.check_post({'make_type': ''}, url, 400) + minimal_post = {'make_type': '', 'day_comment': ''} + self.check_minimal_inputs(url, minimal_post) # to next check illegal new_todo values, we need an actual Process self.post_exp_process([], {}, 1) # check illegal new_todo values - post: dict[str, object] - post = {'make_type': '', 'day_comment': '', 'new_todo': ['foo']} - self.check_post(post, url, 400) - post['new_todo'] = [1, 2] # no Process of .id_=2 exists + self.check_post(minimal_post | {'new_todo': ['foo']}, url, 400) + self.check_post(minimal_post | {'new_todo': [1, 2]}, url, 404) # to next check illegal old_todo inputs, we need to first post Todo - post['new_todo'] = [1] - self.check_post(post, url, 302, '/day?date=2024-01-01&make_type=') + self.check_post(minimal_post | {'new_todo': [1]}, url, 302, + '/day?date=2024-01-01&make_type=') # check illegal old_todo inputs (equal list lengths though) - post = {'make_type': '', 'day_comment': '', 'comment': ['foo'], - 'effort': [3.3], 'done': [], 'todo_id': [1]} + post = minimal_post | {'comment': ['foo'], 'effort': [3.3], + 'done': [], 'todo_id': [1]} self.check_post(post, url, 302, '/day?date=2024-01-01&make_type=') post['todo_id'] = [2] # reference to non-existant Process self.check_post(post, url, 404) @@ -268,7 +258,7 @@ class TestsWithServer(TestCaseWithServer): for i, proc_post in enumerate(proc_posts): self.post_exp_process([exp], proc_post, i+1) self.check_json_get(f'/day?date={date}', exp) - # post Todos of either process and check their display + # post Todos of either Process and check their display self.post_exp_day([exp], {'new_todo': [1, 2]}) self.check_json_get(f'/day?date={date}', exp) # test malformed Todo manipulation posts @@ -302,7 +292,8 @@ class TestsWithServer(TestCaseWithServer): # create two Processes, with second one step of first one self.post_exp_process([exp], {}, 2) self.post_exp_process([exp], {'new_top_step': 2}, 1) - exp.lib_set('ProcessStep', [exp.procstep_as_dict(1, 1, 2, None)]) + exp.lib_set('ProcessStep', [ + exp.procstep_as_dict(1, owner_id=1, step_process_id=2)]) self.check_json_get(f'/day?date={date}', exp) # post Todo of adopting Process, with make_type=full self.post_exp_day([exp], {'make_type': 'full', 'new_todo': [1]}) @@ -325,7 +316,7 @@ class TestsWithServer(TestCaseWithServer): 'children': []}]}] exp.force('top_nodes', top_nodes) self.check_json_get(f'/day?date={date}', exp) - # post another Todo of adopting Process, make_type=empty + # post another Todo of adopting Process, no adopt with make_type=empty self.post_exp_day([exp], {'make_type': 'empty', 'new_todo': [1]}) exp.lib_set('Todo', [exp.todo_as_dict(4, 1)]) top_nodes += [{'todo': 4, @@ -340,7 +331,8 @@ class TestsWithServer(TestCaseWithServer): exp = ExpectedGetDay(date) self.post_exp_process([exp], {}, 2) self.post_exp_process([exp], {'new_top_step': 2}, 1) - exp.lib_set('ProcessStep', [exp.procstep_as_dict(1, 1, 2, None)]) + exp.lib_set('ProcessStep', [ + exp.procstep_as_dict(1, owner_id=1, step_process_id=2)]) # make-full-day-post batch of Todos of both Processes in one order …, self.post_exp_day([exp], {'make_type': 'full', 'new_todo': [1, 2]}) top_nodes: list[dict[str, Any]] = [{'todo': 1, @@ -364,6 +356,23 @@ class TestsWithServer(TestCaseWithServer): exp.lib_get('Todo', 3)['children'] = [4] self.check_json_get(f'/day?date={date}', exp) + def test_POST_day_todo_deletion_by_negative_effort(self) -> None: + """Test POST /day removal of Todos by setting negative effort.""" + date = '2024-01-01' + exp = ExpectedGetDay(date) + self.post_exp_process([exp], {}, 1) + self.post_exp_day([exp], {'new_todo': [1]}) + # check cannot remove Todo if commented + self.post_exp_day([exp], + {'todo_id': [1], 'comment': ['foo'], 'effort': [-1]}) + self.check_json_get(f'/day?date={date}', exp) + # check *can* remove Todo while getting done + self.post_exp_day([exp], + {'todo_id': [1], 'comment': [''], 'effort': [-1], + 'done': [1]}) + exp.lib_del('Todo', 1) + self.check_json_get(f'/day?date={date}', exp) + def test_GET_day_with_conditions(self) -> None: """Test GET /day displaying Conditions and their relations.""" date = '2024-01-01' diff --git a/tests/processes.py b/tests/processes.py index 2561fbb..0c2c1a9 100644 --- a/tests/processes.py +++ b/tests/processes.py @@ -300,10 +300,7 @@ class TestsWithServer(TestCaseWithServer): """Test POST /process and its effect on the database.""" valid_post = {'title': '', 'description': '', 'effort': 1.0} # check payloads lacking minimum expecteds - self.check_post({}, '/process', 400) - self.check_post({'title': '', 'description': ''}, '/process', 400) - self.check_post({'title': '', 'effort': 1}, '/process', 400) - self.check_post({'description': '', 'effort': 1}, '/process', 400) + self.check_minimal_inputs('/process', valid_post) # check payloads of bad data types self.check_post(valid_post | {'effort': ''}, '/process', 400) # check references to non-existant items diff --git a/tests/todos.py b/tests/todos.py index 9f3874d..6118ab7 100644 --- a/tests/todos.py +++ b/tests/todos.py @@ -251,14 +251,14 @@ class ExpectedGetTodo(Expected): @staticmethod def step_as_dict(node_id: int, - children: list[dict[str, object]], process: int | None = None, todo: int | None = None, fillable: bool = False, + children: None | list[dict[str, object]] = None ) -> dict[str, object]: """Return JSON of TodoOrProcStepsNode to expect.""" return {'node_id': node_id, - 'children': children, + 'children': children if children is not None else [], 'process': process, 'fillable': fillable, 'todo': todo} @@ -371,7 +371,8 @@ class TestsWithServer(TestCaseWithServer): 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)]) + exp.set('steps_todo_to_process', [ + exp.step_as_dict(node_id=1, process=None, 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) @@ -393,19 +394,20 @@ class TestsWithServer(TestCaseWithServer): 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]) + exp.lib_set('ProcessStep', + [exp.procstep_as_dict(1, owner_id=1, step_process_id=2), + exp.procstep_as_dict(2, owner_id=1, step_process_id=3)]) + slots = [ + exp.step_as_dict(node_id=1, process=2, todo=None, fillable=True), + exp.step_as_dict(node_id=2, process=3, todo=None, fillable=True)] + exp.set('steps_todo_to_process', slots) 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_to_1': 5, 'adopt': [4]}, exp) exp.lib_get('Todo', 1)['children'] += [5] - 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]) + slots[0]['todo'] = 4 + slots[1]['todo'] = 5 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' @@ -416,15 +418,14 @@ class TestsWithServer(TestCaseWithServer): # 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]) + exp.lib_set('ProcessStep', + [exp.procstep_as_dict(3, owner_id=3, step_process_id=4)]) + slots[1]['children'] = [exp.step_as_dict( + node_id=3, process=4, todo=None, fillable=True)] 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]) + slots += [exp.step_as_dict( + node_id=4, process=None, todo=6, fillable=False)] self.check_json_get('/todo?id=1', exp) def test_POST_todo_make_empty(self) -> None: @@ -434,29 +435,33 @@ class TestsWithServer(TestCaseWithServer): 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)]) + exp.lib_set('ProcessStep', + [exp.procstep_as_dict(1, owner_id=2, step_process_id=1), + exp.procstep_as_dict(2, owner_id=3, step_process_id=2), + exp.procstep_as_dict(3, owner_id=4, step_process_id=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]) + slots = [exp.step_as_dict( + node_id=1, process=3, todo=None, fillable=True, + children=[exp.step_as_dict( + node_id=2, process=2, todo=None, fillable=False, + children=[exp.step_as_dict( + node_id=3, process=1, todo=None, fillable=False)])])] + exp.set('steps_todo_to_process', slots) self.check_json_get('/todo?id=1', exp) self.check_post({'step_filler_to_1': 'make_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]) + slots[0]['todo'] = 2 + assert isinstance(slots[0]['children'], list) + slots[0]['children'][0]['fillable'] = True 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]) + slots += [exp.step_as_dict( + node_id=4, process=None, todo=3, fillable=False)] 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) @@ -477,20 +482,25 @@ class TestsWithServer(TestCaseWithServer): 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]) + exp.lib_set('ProcessStep', [ + exp.procstep_as_dict(1, owner_id=1, step_process_id=2), + exp.procstep_as_dict(2, owner_id=2, step_process_id=3), + exp.procstep_as_dict(3, owner_id=3, step_process_id=4)]) + slots = [exp.step_as_dict( + node_id=1, process=2, todo=None, fillable=True, + children=[exp.step_as_dict( + node_id=2, process=3, todo=None, fillable=False, + children=[exp.step_as_dict( + node_id=3, process=4, todo=None, fillable=False)])])] + exp.set('steps_todo_to_process', slots) 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]) + exp.lib_set('ProcessStep', [ + exp.procstep_as_dict(4, owner_id=1, step_process_id=4)]) + slots += [exp.step_as_dict( + node_id=4, process=4, todo=None, fillable=True)] self.check_json_get('/todo?id=1', exp) def test_POST_todo_doneness_relations(self) -> None: diff --git a/tests/utils.py b/tests/utils.py index 9d5f88e..e706ec0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -679,7 +679,7 @@ class Expected: for todo in self.lib_all('Todo'): if next_id <= todo['id']: next_id = todo['id'] + 1 - for proc_id in sorted([id_ for id_ in v if id_]): + for proc_id in sorted([id_ for id_ in v if id_]): todo = self.todo_as_dict(next_id, proc_id, date) self.lib_set('Todo', [todo]) next_id += 1 @@ -947,6 +947,15 @@ class TestCaseWithServer(TestCaseWithDB): self.conn.request('GET', target) self.assertEqual(self.conn.getresponse().status, expected_code) + def check_minimal_inputs(self, + url: str, + minimal_inputs: dict[str, Any] + ) -> None: + """Check that url 400's unless all of minimal_inputs provided.""" + for to_hide in minimal_inputs.keys(): + to_post = {k: v for k, v in minimal_inputs.items() if k != to_hide} + self.check_post(to_post, url, 400) + def check_post(self, data: Mapping[str, object], target: str, expected_code: int = 302, redir: str = '') -> None: """Check that POST of data to target yields expected_code.""" -- 2.30.2