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]."""
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 = []
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):
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')
"""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)
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
# 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]})
'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,
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,
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'
"""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
@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}
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)
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'
# 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:
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)
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:
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
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."""