home · contact · privacy
Lots of refactoring.
[plomtask] / tests / todos.py
1 """Test Todos module."""
2 from tests.utils import TestCaseSansDB, TestCaseWithDB, TestCaseWithServer
3 from plomtask.todos import Todo, TodoNode
4 from plomtask.processes import Process, ProcessStep
5 from plomtask.conditions import Condition
6 from plomtask.exceptions import (NotFoundException, BadFormatException,
7                                  HandledException)
8
9
10 class TestsWithDB(TestCaseWithDB, TestCaseSansDB):
11     """Tests requiring DB, but not server setup.
12
13     NB: We subclass TestCaseSansDB too, to run any tests there that due to any
14     Todo requiring a _saved_ Process wouldn't run without a DB.
15     """
16     checked_class = Todo
17     default_init_kwargs = {'process': None, 'is_done': False,
18                            'date': '2024-01-01'}
19
20     def setUp(self) -> None:
21         super().setUp()
22         self.date1 = '2024-01-01'
23         self.date2 = '2024-01-02'
24         self.proc = Process(None)
25         self.proc.save(self.db_conn)
26         self.cond1 = Condition(None)
27         self.cond1.save(self.db_conn)
28         self.cond2 = Condition(None)
29         self.cond2.save(self.db_conn)
30         self.default_init_kwargs['process'] = self.proc
31
32     def test_Todo_init(self) -> None:
33         """Test creation of Todo and what they default to."""
34         process = Process(None)
35         with self.assertRaises(NotFoundException):
36             Todo(None, process, False, self.date1)
37         process.save(self.db_conn)
38         assert isinstance(self.cond1.id_, int)
39         assert isinstance(self.cond2.id_, int)
40         process.set_conditions(self.db_conn, [self.cond1.id_, self.cond2.id_])
41         process.set_enables(self.db_conn, [self.cond1.id_])
42         process.set_disables(self.db_conn, [self.cond2.id_])
43         todo_no_id = Todo(None, process, False, self.date1)
44         self.assertEqual(todo_no_id.conditions, [self.cond1, self.cond2])
45         self.assertEqual(todo_no_id.enables, [self.cond1])
46         self.assertEqual(todo_no_id.disables, [self.cond2])
47         todo_yes_id = Todo(5, process, False, self.date1)
48         self.assertEqual(todo_yes_id.conditions, [])
49         self.assertEqual(todo_yes_id.enables, [])
50         self.assertEqual(todo_yes_id.disables, [])
51
52     def test_Todo_by_date(self) -> None:
53         """Test findability of Todos by date."""
54         t1 = Todo(None, self.proc, False, self.date1)
55         t1.save(self.db_conn)
56         t2 = Todo(None, self.proc, False, self.date1)
57         t2.save(self.db_conn)
58         self.assertEqual(Todo.by_date(self.db_conn, self.date1), [t1, t2])
59         self.assertEqual(Todo.by_date(self.db_conn, self.date2), [])
60         with self.assertRaises(BadFormatException):
61             self.assertEqual(Todo.by_date(self.db_conn, 'foo'), [])
62
63     def test_Todo_on_conditions(self) -> None:
64         """Test effect of Todos on Conditions."""
65         assert isinstance(self.cond1.id_, int)
66         assert isinstance(self.cond2.id_, int)
67         todo = Todo(None, self.proc, False, self.date1)
68         todo.save(self.db_conn)
69         todo.set_enables(self.db_conn, [self.cond1.id_])
70         todo.set_disables(self.db_conn, [self.cond2.id_])
71         todo.is_done = True
72         self.assertEqual(self.cond1.is_active, True)
73         self.assertEqual(self.cond2.is_active, False)
74         todo.is_done = False
75         self.assertEqual(self.cond1.is_active, True)
76         self.assertEqual(self.cond2.is_active, False)
77
78     def test_Todo_children(self) -> None:
79         """Test Todo.children relations."""
80         todo_1 = Todo(None, self.proc, False, self.date1)
81         todo_2 = Todo(None, self.proc, False, self.date1)
82         todo_2.save(self.db_conn)
83         with self.assertRaises(HandledException):
84             todo_1.add_child(todo_2)
85         todo_1.save(self.db_conn)
86         todo_3 = Todo(None, self.proc, False, self.date1)
87         with self.assertRaises(HandledException):
88             todo_1.add_child(todo_3)
89         todo_3.save(self.db_conn)
90         todo_1.add_child(todo_3)
91         todo_1.save(self.db_conn)
92         assert isinstance(todo_1.id_, int)
93         todo_retrieved = Todo.by_id(self.db_conn, todo_1.id_)
94         self.assertEqual(todo_retrieved.children, [todo_3])
95         with self.assertRaises(BadFormatException):
96             todo_3.add_child(todo_1)
97
98     def test_Todo_conditioning(self) -> None:
99         """Test Todo.doability conditions."""
100         assert isinstance(self.cond1.id_, int)
101         todo_1 = Todo(None, self.proc, False, self.date1)
102         todo_1.save(self.db_conn)
103         todo_2 = Todo(None, self.proc, False, self.date1)
104         todo_2.save(self.db_conn)
105         todo_2.add_child(todo_1)
106         with self.assertRaises(BadFormatException):
107             todo_2.is_done = True
108         todo_1.is_done = True
109         todo_2.is_done = True
110         todo_2.is_done = False
111         todo_2.set_conditions(self.db_conn, [self.cond1.id_])
112         with self.assertRaises(BadFormatException):
113             todo_2.is_done = True
114         self.cond1.is_active = True
115         todo_2.is_done = True
116
117     def test_Todo_step_tree(self) -> None:
118         """Test self-configuration of TodoStepsNode tree for Day view."""
119         todo_1 = Todo(None, self.proc, False, self.date1)
120         todo_1.save(self.db_conn)
121         assert isinstance(todo_1.id_, int)
122         # test minimum
123         node_0 = TodoNode(todo_1, False, [])
124         self.assertEqual(todo_1.get_step_tree(set()).as_dict, node_0.as_dict)
125         # test non_emtpy seen_todo does something
126         node_0.seen = True
127         self.assertEqual(todo_1.get_step_tree({todo_1.id_}).as_dict,
128                          node_0.as_dict)
129         # test child shows up
130         todo_2 = Todo(None, self.proc, False, self.date1)
131         todo_2.save(self.db_conn)
132         assert isinstance(todo_2.id_, int)
133         todo_1.add_child(todo_2)
134         node_2 = TodoNode(todo_2, False, [])
135         node_0.children = [node_2]
136         node_0.seen = False
137         self.assertEqual(todo_1.get_step_tree(set()).as_dict, node_0.as_dict)
138         # test child shows up with child
139         todo_3 = Todo(None, self.proc, False, self.date1)
140         todo_3.save(self.db_conn)
141         assert isinstance(todo_3.id_, int)
142         todo_2.add_child(todo_3)
143         node_3 = TodoNode(todo_3, False, [])
144         node_2.children = [node_3]
145         self.assertEqual(todo_1.get_step_tree(set()).as_dict, node_0.as_dict)
146         # test same todo can be child-ed multiple times at different locations
147         todo_1.add_child(todo_3)
148         node_4 = TodoNode(todo_3, True, [])
149         node_0.children += [node_4]
150         self.assertEqual(todo_1.get_step_tree(set()).as_dict, node_0.as_dict)
151
152     def test_Todo_create_with_children(self) -> None:
153         """Test parenthood guaranteeds of Todo.create_with_children."""
154         assert isinstance(self.proc.id_, int)
155         proc2 = Process(None)
156         proc2.save(self.db_conn)
157         assert isinstance(proc2.id_, int)
158         proc3 = Process(None)
159         proc3.save(self.db_conn)
160         assert isinstance(proc3.id_, int)
161         proc4 = Process(None)
162         proc4.save(self.db_conn)
163         assert isinstance(proc4.id_, int)
164         # make proc4 step of proc3
165         step = ProcessStep(None, proc3.id_, proc4.id_, None)
166         proc3.set_steps(self.db_conn, [step])
167         # give proc2 three steps; 2× proc1, 1× proc3
168         step1 = ProcessStep(None, proc2.id_, self.proc.id_, None)
169         step2 = ProcessStep(None, proc2.id_, self.proc.id_, None)
170         step3 = ProcessStep(None, proc2.id_, proc3.id_, None)
171         proc2.set_steps(self.db_conn, [step1, step2, step3])
172         # test mere creation does nothing
173         todo_ignore = Todo(None, proc2, False, self.date1)
174         todo_ignore.save(self.db_conn)
175         self.assertEqual(todo_ignore.children, [])
176         # test create_with_children on step-less does nothing
177         todo_1 = Todo.create_with_children(self.db_conn, self.proc.id_,
178                                            self.date1)
179         self.assertEqual(todo_1.children, [])
180         self.assertEqual(len(Todo.all(self.db_conn)), 2)
181         # test create_with_children adopts and creates, and down tree too
182         todo_2 = Todo.create_with_children(self.db_conn, proc2.id_, self.date1)
183         self.assertEqual(3, len(todo_2.children))
184         self.assertEqual(todo_1, todo_2.children[0])
185         self.assertEqual(self.proc, todo_2.children[2].process)
186         self.assertEqual(proc3, todo_2.children[1].process)
187         todo_3 = todo_2.children[1]
188         self.assertEqual(len(todo_3.children), 1)
189         self.assertEqual(todo_3.children[0].process, proc4)
190
191     def test_Todo_remove(self) -> None:
192         """Test removal."""
193         todo_1 = Todo(None, self.proc, False, self.date1)
194         todo_1.save(self.db_conn)
195         assert todo_1.id_ is not None
196         todo_0 = Todo(None, self.proc, False, self.date1)
197         todo_0.save(self.db_conn)
198         todo_0.add_child(todo_1)
199         todo_2 = Todo(None, self.proc, False, self.date1)
200         todo_2.save(self.db_conn)
201         todo_1.add_child(todo_2)
202         todo_1_id = todo_1.id_
203         todo_1.remove(self.db_conn)
204         with self.assertRaises(NotFoundException):
205             Todo.by_id(self.db_conn, todo_1_id)
206         self.assertEqual(todo_0.children, [])
207         self.assertEqual(todo_2.parents, [])
208         todo_2.comment = 'foo'
209         with self.assertRaises(HandledException):
210             todo_2.remove(self.db_conn)
211         todo_2.comment = ''
212         todo_2.effort = 5
213         with self.assertRaises(HandledException):
214             todo_2.remove(self.db_conn)
215
216     def test_Todo_autoremoval(self) -> None:
217         """"Test automatic removal for Todo.effort < 0."""
218         todo_1 = Todo(None, self.proc, False, self.date1)
219         todo_1.save(self.db_conn)
220         todo_1.comment = 'foo'
221         todo_1.effort = -0.1
222         todo_1.save(self.db_conn)
223         assert todo_1.id_ is not None
224         Todo.by_id(self.db_conn, todo_1.id_)
225         todo_1.comment = ''
226         todo_1_id = todo_1.id_
227         todo_1.save(self.db_conn)
228         with self.assertRaises(NotFoundException):
229             Todo.by_id(self.db_conn, todo_1_id)
230
231
232 class TestsWithServer(TestCaseWithServer):
233     """Tests against our HTTP server/handler (and database)."""
234
235     def test_do_POST_day(self) -> None:
236         """Test Todo posting of POST /day."""
237         self.post_process()
238         self.post_process(2)
239         proc = Process.by_id(self.db_conn, 1)
240         proc2 = Process.by_id(self.db_conn, 2)
241         form_data = {'day_comment': '', 'make_type': 'full'}
242         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
243         self.assertEqual(Todo.by_date(self.db_conn, '2024-01-01'), [])
244         proc = Process.by_id(self.db_conn, 1)
245         form_data['new_todo'] = str(proc.id_)
246         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
247         todos = Todo.by_date(self.db_conn, '2024-01-01')
248         self.assertEqual(1, len(todos))
249         todo1 = todos[0]
250         self.assertEqual(todo1.id_, 1)
251         proc = Process.by_id(self.db_conn, 1)
252         self.assertEqual(todo1.process.id_, proc.id_)
253         self.assertEqual(todo1.is_done, False)
254         proc2 = Process.by_id(self.db_conn, 2)
255         form_data['new_todo'] = str(proc2.id_)
256         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
257         todos = Todo.by_date(self.db_conn, '2024-01-01')
258         todo1 = todos[1]
259         self.assertEqual(todo1.id_, 2)
260         proc2 = Process.by_id(self.db_conn, 1)
261         todo1 = Todo.by_date(self.db_conn, '2024-01-01')[0]
262         self.assertEqual(todo1.id_, 1)
263         self.assertEqual(todo1.process.id_, proc2.id_)
264         self.assertEqual(todo1.is_done, False)
265
266     def test_do_POST_todo(self) -> None:
267         """Test POST /todo."""
268         def post_and_reload(form_data: dict[str, object], status: int = 302,
269                             redir_url: str = '/todo?id=1') -> Todo:
270             self.check_post(form_data, '/todo?id=1', status, redir_url)
271             return Todo.by_date(self.db_conn, '2024-01-01')[0]
272         # test minimum
273         self.post_process()
274         self.check_post({'day_comment': '', 'new_todo': 1,
275                          'make_type': 'full'},
276                         '/day?date=2024-01-01&make_type=full', 302)
277         # test posting to bad URLs
278         self.check_post({}, '/todo=', 404)
279         self.check_post({}, '/todo?id=', 404)
280         self.check_post({}, '/todo?id=FOO', 400)
281         self.check_post({}, '/todo?id=0', 404)
282         # test posting naked entity
283         todo1 = post_and_reload({})
284         self.assertEqual(todo1.children, [])
285         self.assertEqual(todo1.parents, [])
286         self.assertEqual(todo1.is_done, False)
287         # test posting doneness
288         todo1 = post_and_reload({'done': ''})
289         self.assertEqual(todo1.is_done, True)
290         # test implicitly posting non-doneness
291         todo1 = post_and_reload({})
292         self.assertEqual(todo1.is_done, False)
293         # test malformed adoptions
294         self.check_post({'adopt': 'foo'}, '/todo?id=1', 400)
295         self.check_post({'adopt': 1}, '/todo?id=1', 400)
296         self.check_post({'adopt': 2}, '/todo?id=1', 404)
297         # test posting second todo of same process
298         self.check_post({'day_comment': '', 'new_todo': 1,
299                          'make_type': 'full'},
300                         '/day?date=2024-01-01&make_type=full', 302)
301         # test todo 1 adopting todo 2
302         todo1 = post_and_reload({'adopt': 2})
303         todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
304         self.assertEqual(todo1.children, [todo2])
305         self.assertEqual(todo1.parents, [])
306         self.assertEqual(todo2.children, [])
307         self.assertEqual(todo2.parents, [todo1])
308         # test todo1 cannot be set done with todo2 not done yet
309         todo1 = post_and_reload({'done': '', 'adopt': 2}, 400)
310         self.assertEqual(todo1.is_done, False)
311         # test todo1 un-adopting todo 2 by just not sending an adopt
312         todo1 = post_and_reload({}, 302)
313         todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
314         self.assertEqual(todo1.children, [])
315         self.assertEqual(todo1.parents, [])
316         self.assertEqual(todo2.children, [])
317         self.assertEqual(todo2.parents, [])
318         # test todo1 deletion
319         todo1 = post_and_reload({'delete': ''}, 302, '/')
320
321     def test_do_POST_day_todo_adoption(self) -> None:
322         """Test Todos posted to Day view may adopt existing Todos."""
323         form_data = self.post_process()
324         form_data = self.post_process(2, form_data | {'new_top_step': 1})
325         form_data = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'}
326         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
327         form_data['new_todo'] = 2
328         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
329         todo1 = Todo.by_date(self.db_conn, '2024-01-01')[0]
330         todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
331         self.assertEqual(todo1.children, [])
332         self.assertEqual(todo1.parents, [todo2])
333         self.assertEqual(todo2.children, [todo1])
334         self.assertEqual(todo2.parents, [])
335
336     def test_do_POST_day_todo_multiple(self) -> None:
337         """Test multiple Todos can be posted to Day view."""
338         form_data = self.post_process()
339         form_data = self.post_process(2)
340         form_data = {'day_comment': '', 'new_todo': [1, 2],
341                      'make_type': 'full'}
342         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
343         todo1 = Todo.by_date(self.db_conn, '2024-01-01')[0]
344         todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
345         self.assertEqual(todo1.process.id_, 1)
346         self.assertEqual(todo2.process.id_, 2)
347
348     def test_do_POST_day_todo_multiple_inner_adoption(self) -> None:
349         """Test multiple Todos can be posted to Day view w. inner adoption."""
350
351         def key_order_func(t: Todo) -> int:
352             assert isinstance(t.process.id_, int)
353             return t.process.id_
354
355         def check_adoption(date: str, new_todos: list[int]) -> None:
356             form_data = {'day_comment': '', 'new_todo': new_todos,
357                          'make_type': 'full'}
358             self.check_post(form_data, f'/day?date={date}&make_type=full', 302)
359             day_todos = Todo.by_date(self.db_conn, date)
360             day_todos.sort(key=key_order_func)
361             todo1 = day_todos[0]
362             todo2 = day_todos[1]
363             self.assertEqual(todo1.children, [])
364             self.assertEqual(todo1.parents, [todo2])
365             self.assertEqual(todo2.children, [todo1])
366             self.assertEqual(todo2.parents, [])
367
368         def check_nesting_adoption(process_id: int, date: str,
369                                    new_top_steps: list[int]) -> None:
370             form_data = {'title': '', 'description': '', 'effort': 1,
371                          'step_of': [2]}
372             form_data = self.post_process(1, form_data)
373             form_data['new_top_step'] = new_top_steps
374             form_data['step_of'] = []
375             form_data = self.post_process(process_id, form_data)
376             form_data = {'day_comment': '', 'new_todo': [process_id],
377                          'make_type': 'full'}
378             self.check_post(form_data, f'/day?date={date}&make_type=full', 302)
379             day_todos = Todo.by_date(self.db_conn, date)
380             day_todos.sort(key=key_order_func, reverse=True)
381             self.assertEqual(len(day_todos), 3)
382             todo1 = day_todos[0]  # process of process_id
383             todo2 = day_todos[1]  # process 2
384             todo3 = day_todos[2]  # process 1
385             self.assertEqual(sorted(todo1.children), sorted([todo2, todo3]))
386             self.assertEqual(todo1.parents, [])
387             self.assertEqual(todo2.children, [todo3])
388             self.assertEqual(todo2.parents, [todo1])
389             self.assertEqual(todo3.children, [])
390             self.assertEqual(sorted(todo3.parents), sorted([todo2, todo1]))
391
392         form_data = self.post_process()
393         form_data = self.post_process(2, form_data | {'new_top_step': 1})
394         check_adoption('2024-01-01', [1, 2])
395         check_adoption('2024-01-02', [2, 1])
396         check_nesting_adoption(3, '2024-01-03', [1, 2])
397         check_nesting_adoption(4, '2024-01-04', [2, 1])
398
399     def test_do_POST_day_todo_doneness(self) -> None:
400         """Test Todo doneness can be posted to Day view."""
401         self.post_process()
402         form_data = {'day_comment': '', 'new_todo': [1], 'make_type': 'full'}
403         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
404         todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
405         form_data = {'day_comment': '', 'todo_id': [1], 'make_type': 'full',
406                      'comment': [''], 'done': [], 'effort': ['']}
407         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
408         todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
409         self.assertEqual(todo.is_done, False)
410         form_data = {'day_comment': '', 'todo_id': [1], 'done': [1],
411                      'make_type': 'full', 'comment': [''], 'effort': ['']}
412         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
413         todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
414         self.assertEqual(todo.is_done, True)
415
416     def test_do_GET_todo(self) -> None:
417         """Test GET /todo response codes."""
418         self.post_process()
419         form_data = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'}
420         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
421         self.check_get('/todo', 404)
422         self.check_get('/todo?id=', 404)
423         self.check_get('/todo?id=foo', 400)
424         self.check_get('/todo?id=0', 404)
425         self.check_get('/todo?id=1', 200)