microsoft-todo-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- microsoft_todo_cli-1.0.0.dist-info/METADATA +296 -0
- microsoft_todo_cli-1.0.0.dist-info/RECORD +37 -0
- microsoft_todo_cli-1.0.0.dist-info/WHEEL +5 -0
- microsoft_todo_cli-1.0.0.dist-info/entry_points.txt +2 -0
- microsoft_todo_cli-1.0.0.dist-info/licenses/LICENSE +22 -0
- microsoft_todo_cli-1.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/run_tests.py +39 -0
- tests/test_checklist_cli.py +118 -0
- tests/test_checklist_item_model.py +108 -0
- tests/test_checklist_wrapper.py +62 -0
- tests/test_cli_commands.py +436 -0
- tests/test_cli_output.py +169 -0
- tests/test_cli_url_integration.py +136 -0
- tests/test_datetime_parser.py +233 -0
- tests/test_filters.py +120 -0
- tests/test_json_output.py +223 -0
- tests/test_lst_output.py +135 -0
- tests/test_models.py +235 -0
- tests/test_odata_escape.py +175 -0
- tests/test_recurrence.py +159 -0
- tests/test_update_command.py +192 -0
- tests/test_utils.py +186 -0
- tests/test_wrapper.py +191 -0
- todocli/__init__.py +1 -0
- todocli/cli.py +1356 -0
- todocli/graphapi/__init__.py +0 -0
- todocli/graphapi/oauth.py +136 -0
- todocli/graphapi/wrapper.py +660 -0
- todocli/models/__init__.py +0 -0
- todocli/models/checklistitem.py +59 -0
- todocli/models/todolist.py +27 -0
- todocli/models/todotask.py +105 -0
- todocli/utils/__init__.py +0 -0
- todocli/utils/datetime_util.py +321 -0
- todocli/utils/recurrence_util.py +122 -0
- todocli/utils/update_checker.py +55 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for OData string escaping in graph API wrapper"""
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch, MagicMock
|
|
6
|
+
from todocli.graphapi.wrapper import (
|
|
7
|
+
_escape_odata_string,
|
|
8
|
+
get_task_id_by_name,
|
|
9
|
+
TaskNotFoundByName,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestEscapeOdataString(unittest.TestCase):
|
|
14
|
+
"""Test _escape_odata_string helper"""
|
|
15
|
+
|
|
16
|
+
def test_plain_text_unchanged(self):
|
|
17
|
+
"""Test that plain text passes through unchanged"""
|
|
18
|
+
self.assertEqual(_escape_odata_string("Buy milk"), "Buy milk")
|
|
19
|
+
|
|
20
|
+
def test_cyrillic_text_unchanged(self):
|
|
21
|
+
"""Test that Cyrillic text passes through unchanged"""
|
|
22
|
+
self.assertEqual(
|
|
23
|
+
_escape_odata_string("Купить молоко"),
|
|
24
|
+
"Купить молоко",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def test_single_quote_doubled(self):
|
|
28
|
+
"""Test that single quotes are doubled per OData spec"""
|
|
29
|
+
self.assertEqual(_escape_odata_string("it's done"), "it''s done")
|
|
30
|
+
|
|
31
|
+
def test_multiple_single_quotes(self):
|
|
32
|
+
"""Test multiple single quotes in one string"""
|
|
33
|
+
self.assertEqual(
|
|
34
|
+
_escape_odata_string("it's Alice's task"),
|
|
35
|
+
"it''s Alice''s task",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def test_hash_double_encoded(self):
|
|
39
|
+
"""Test that '#' is double-percent-encoded for URL safety"""
|
|
40
|
+
self.assertEqual(
|
|
41
|
+
_escape_odata_string("task #tag"),
|
|
42
|
+
"task %2523tag",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def test_ampersand_double_encoded(self):
|
|
46
|
+
"""Test that '&' is double-percent-encoded for URL safety"""
|
|
47
|
+
self.assertEqual(
|
|
48
|
+
_escape_odata_string("bread & butter"),
|
|
49
|
+
"bread %2526 butter",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def test_plus_double_encoded(self):
|
|
53
|
+
"""Test that '+' is double-percent-encoded for URL safety"""
|
|
54
|
+
self.assertEqual(
|
|
55
|
+
_escape_odata_string("1+1=2"),
|
|
56
|
+
"1%252B1=2",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def test_all_special_chars_combined(self):
|
|
60
|
+
"""Test string with all special characters at once"""
|
|
61
|
+
self.assertEqual(
|
|
62
|
+
_escape_odata_string("it's a #tag & 1+1"),
|
|
63
|
+
"it''s a %2523tag %2526 1%252B1",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def test_empty_string(self):
|
|
67
|
+
"""Test that empty string returns empty string"""
|
|
68
|
+
self.assertEqual(_escape_odata_string(""), "")
|
|
69
|
+
|
|
70
|
+
def test_only_special_chars(self):
|
|
71
|
+
"""Test string consisting entirely of special characters"""
|
|
72
|
+
self.assertEqual(_escape_odata_string("#&#"), "%2523%2526%2523")
|
|
73
|
+
|
|
74
|
+
def test_safe_url_chars_unchanged(self):
|
|
75
|
+
"""Test that other URL-safe special chars are not affected"""
|
|
76
|
+
for char in ["?", "=", "/", ":", "(", ")"]:
|
|
77
|
+
value = f"task {char} test"
|
|
78
|
+
self.assertEqual(
|
|
79
|
+
_escape_odata_string(value),
|
|
80
|
+
value,
|
|
81
|
+
f"Character '{char}' should not be escaped",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestGetTaskIdByNameEndpoint(unittest.TestCase):
|
|
86
|
+
"""Test that get_task_id_by_name builds the correct OData filter URL"""
|
|
87
|
+
|
|
88
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
89
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
90
|
+
def test_plain_name_filter(self, mock_get_list_id, mock_get_session):
|
|
91
|
+
"""Test OData filter URL for a plain task name"""
|
|
92
|
+
mock_get_list_id.return_value = "list123"
|
|
93
|
+
mock_response = MagicMock()
|
|
94
|
+
mock_response.content = b'{"value": [{"id": "task1", "title": "Buy milk", "importance": "normal", "status": "notStarted", "createdDateTime": "2024-01-01T00:00:00.0000000Z", "lastModifiedDateTime": "2024-01-01T00:00:00.0000000Z", "isReminderOn": false}]}'
|
|
95
|
+
mock_get_session.return_value.get.return_value = mock_response
|
|
96
|
+
|
|
97
|
+
get_task_id_by_name("Tasks", "Buy milk")
|
|
98
|
+
|
|
99
|
+
called_url = mock_get_session.return_value.get.call_args[0][0]
|
|
100
|
+
self.assertIn("$filter=title eq 'Buy milk'", called_url)
|
|
101
|
+
|
|
102
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
103
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
104
|
+
def test_hash_tag_filter(self, mock_get_list_id, mock_get_session):
|
|
105
|
+
"""Test OData filter URL for task name containing '#' (tag)"""
|
|
106
|
+
mock_get_list_id.return_value = "list123"
|
|
107
|
+
mock_response = MagicMock()
|
|
108
|
+
mock_response.content = b'{"value": [{"id": "task1", "title": "Do stuff #work", "importance": "normal", "status": "notStarted", "createdDateTime": "2024-01-01T00:00:00.0000000Z", "lastModifiedDateTime": "2024-01-01T00:00:00.0000000Z", "isReminderOn": false}]}'
|
|
109
|
+
mock_get_session.return_value.get.return_value = mock_response
|
|
110
|
+
|
|
111
|
+
get_task_id_by_name("Tasks", "Do stuff #work")
|
|
112
|
+
|
|
113
|
+
called_url = mock_get_session.return_value.get.call_args[0][0]
|
|
114
|
+
self.assertIn("%2523", called_url)
|
|
115
|
+
self.assertNotIn("#work", called_url)
|
|
116
|
+
|
|
117
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
118
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
119
|
+
def test_ampersand_filter(self, mock_get_list_id, mock_get_session):
|
|
120
|
+
"""Test OData filter URL for task name containing '&'"""
|
|
121
|
+
mock_get_list_id.return_value = "list123"
|
|
122
|
+
mock_response = MagicMock()
|
|
123
|
+
mock_response.content = b'{"value": [{"id": "task1", "title": "bread & butter", "importance": "normal", "status": "notStarted", "createdDateTime": "2024-01-01T00:00:00.0000000Z", "lastModifiedDateTime": "2024-01-01T00:00:00.0000000Z", "isReminderOn": false}]}'
|
|
124
|
+
mock_get_session.return_value.get.return_value = mock_response
|
|
125
|
+
|
|
126
|
+
get_task_id_by_name("Tasks", "bread & butter")
|
|
127
|
+
|
|
128
|
+
called_url = mock_get_session.return_value.get.call_args[0][0]
|
|
129
|
+
self.assertIn("%2526", called_url)
|
|
130
|
+
self.assertNotIn("& butter", called_url)
|
|
131
|
+
|
|
132
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
133
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
134
|
+
def test_plus_filter(self, mock_get_list_id, mock_get_session):
|
|
135
|
+
"""Test OData filter URL for task name containing '+'"""
|
|
136
|
+
mock_get_list_id.return_value = "list123"
|
|
137
|
+
mock_response = MagicMock()
|
|
138
|
+
mock_response.content = b'{"value": [{"id": "task1", "title": "1+1=2", "importance": "normal", "status": "notStarted", "createdDateTime": "2024-01-01T00:00:00.0000000Z", "lastModifiedDateTime": "2024-01-01T00:00:00.0000000Z", "isReminderOn": false}]}'
|
|
139
|
+
mock_get_session.return_value.get.return_value = mock_response
|
|
140
|
+
|
|
141
|
+
get_task_id_by_name("Tasks", "1+1=2")
|
|
142
|
+
|
|
143
|
+
called_url = mock_get_session.return_value.get.call_args[0][0]
|
|
144
|
+
self.assertIn("%252B", called_url)
|
|
145
|
+
self.assertNotIn("+1", called_url)
|
|
146
|
+
|
|
147
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
148
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
149
|
+
def test_single_quote_filter(self, mock_get_list_id, mock_get_session):
|
|
150
|
+
"""Test OData filter URL for task name containing single quote"""
|
|
151
|
+
mock_get_list_id.return_value = "list123"
|
|
152
|
+
mock_response = MagicMock()
|
|
153
|
+
mock_response.content = b'{"value": [{"id": "task1", "title": "it\'s done", "importance": "normal", "status": "notStarted", "createdDateTime": "2024-01-01T00:00:00.0000000Z", "lastModifiedDateTime": "2024-01-01T00:00:00.0000000Z", "isReminderOn": false}]}'
|
|
154
|
+
mock_get_session.return_value.get.return_value = mock_response
|
|
155
|
+
|
|
156
|
+
get_task_id_by_name("Tasks", "it's done")
|
|
157
|
+
|
|
158
|
+
called_url = mock_get_session.return_value.get.call_args[0][0]
|
|
159
|
+
self.assertIn("it''s done", called_url)
|
|
160
|
+
|
|
161
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
162
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
163
|
+
def test_not_found_raises_exception(self, mock_get_list_id, mock_get_session):
|
|
164
|
+
"""Test that TaskNotFoundByName is raised for non-existent task"""
|
|
165
|
+
mock_get_list_id.return_value = "list123"
|
|
166
|
+
mock_response = MagicMock()
|
|
167
|
+
mock_response.content = b'{"value": []}'
|
|
168
|
+
mock_get_session.return_value.get.return_value = mock_response
|
|
169
|
+
|
|
170
|
+
with self.assertRaises(TaskNotFoundByName):
|
|
171
|
+
get_task_id_by_name("Tasks", "NonExistent")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
unittest.main()
|
tests/test_recurrence.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for recurrence expression parsing"""
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from datetime import date
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
from todocli.utils.recurrence_util import parse_recurrence, InvalidRecurrenceExpression
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestParseRecurrence(unittest.TestCase):
|
|
12
|
+
"""Test parse_recurrence function"""
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
self.today = date(2026, 2, 4)
|
|
16
|
+
self.patcher = patch(
|
|
17
|
+
"todocli.utils.recurrence_util.date",
|
|
18
|
+
wraps=date,
|
|
19
|
+
)
|
|
20
|
+
self.mock_date = self.patcher.start()
|
|
21
|
+
self.mock_date.today.return_value = self.today
|
|
22
|
+
|
|
23
|
+
def tearDown(self):
|
|
24
|
+
self.patcher.stop()
|
|
25
|
+
|
|
26
|
+
def test_none_input(self):
|
|
27
|
+
"""Test None input returns None"""
|
|
28
|
+
self.assertIsNone(parse_recurrence(None))
|
|
29
|
+
|
|
30
|
+
def test_empty_string(self):
|
|
31
|
+
"""Test empty string returns None"""
|
|
32
|
+
self.assertIsNone(parse_recurrence(""))
|
|
33
|
+
self.assertIsNone(parse_recurrence(" "))
|
|
34
|
+
|
|
35
|
+
def test_daily(self):
|
|
36
|
+
"""Test 'daily' preset"""
|
|
37
|
+
result = parse_recurrence("daily")
|
|
38
|
+
self.assertEqual(result["pattern"]["type"], "daily")
|
|
39
|
+
self.assertEqual(result["pattern"]["interval"], 1)
|
|
40
|
+
self.assertEqual(result["range"]["type"], "noEnd")
|
|
41
|
+
self.assertEqual(result["range"]["startDate"], "2026-02-04")
|
|
42
|
+
|
|
43
|
+
def test_weekly(self):
|
|
44
|
+
"""Test 'weekly' preset"""
|
|
45
|
+
result = parse_recurrence("weekly")
|
|
46
|
+
self.assertEqual(result["pattern"]["type"], "weekly")
|
|
47
|
+
self.assertEqual(result["pattern"]["interval"], 1)
|
|
48
|
+
|
|
49
|
+
def test_weekdays(self):
|
|
50
|
+
"""Test 'weekdays' preset sets mon-fri"""
|
|
51
|
+
result = parse_recurrence("weekdays")
|
|
52
|
+
self.assertEqual(result["pattern"]["type"], "weekly")
|
|
53
|
+
self.assertEqual(result["pattern"]["interval"], 1)
|
|
54
|
+
self.assertEqual(
|
|
55
|
+
result["pattern"]["daysOfWeek"],
|
|
56
|
+
["monday", "tuesday", "wednesday", "thursday", "friday"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def test_monthly(self):
|
|
60
|
+
"""Test 'monthly' preset sets dayOfMonth from today"""
|
|
61
|
+
result = parse_recurrence("monthly")
|
|
62
|
+
self.assertEqual(result["pattern"]["type"], "absoluteMonthly")
|
|
63
|
+
self.assertEqual(result["pattern"]["interval"], 1)
|
|
64
|
+
self.assertEqual(result["pattern"]["dayOfMonth"], 4)
|
|
65
|
+
|
|
66
|
+
def test_yearly(self):
|
|
67
|
+
"""Test 'yearly' preset sets dayOfMonth and month from today"""
|
|
68
|
+
result = parse_recurrence("yearly")
|
|
69
|
+
self.assertEqual(result["pattern"]["type"], "absoluteYearly")
|
|
70
|
+
self.assertEqual(result["pattern"]["interval"], 1)
|
|
71
|
+
self.assertEqual(result["pattern"]["dayOfMonth"], 4)
|
|
72
|
+
self.assertEqual(result["pattern"]["month"], 2)
|
|
73
|
+
|
|
74
|
+
def test_every_n_days(self):
|
|
75
|
+
"""Test 'every 2 days' custom interval"""
|
|
76
|
+
result = parse_recurrence("every 2 days")
|
|
77
|
+
self.assertEqual(result["pattern"]["type"], "daily")
|
|
78
|
+
self.assertEqual(result["pattern"]["interval"], 2)
|
|
79
|
+
|
|
80
|
+
def test_every_n_weeks(self):
|
|
81
|
+
"""Test 'every 3 weeks' custom interval"""
|
|
82
|
+
result = parse_recurrence("every 3 weeks")
|
|
83
|
+
self.assertEqual(result["pattern"]["type"], "weekly")
|
|
84
|
+
self.assertEqual(result["pattern"]["interval"], 3)
|
|
85
|
+
|
|
86
|
+
def test_every_n_months(self):
|
|
87
|
+
"""Test 'every 2 months' custom interval"""
|
|
88
|
+
result = parse_recurrence("every 2 months")
|
|
89
|
+
self.assertEqual(result["pattern"]["type"], "absoluteMonthly")
|
|
90
|
+
self.assertEqual(result["pattern"]["interval"], 2)
|
|
91
|
+
self.assertEqual(result["pattern"]["dayOfMonth"], 4)
|
|
92
|
+
|
|
93
|
+
def test_every_n_years(self):
|
|
94
|
+
"""Test 'every 1 year' custom interval"""
|
|
95
|
+
result = parse_recurrence("every 1 year")
|
|
96
|
+
self.assertEqual(result["pattern"]["type"], "absoluteYearly")
|
|
97
|
+
self.assertEqual(result["pattern"]["interval"], 1)
|
|
98
|
+
|
|
99
|
+
def test_weekly_with_days(self):
|
|
100
|
+
"""Test 'weekly:mon,fri' with day specifiers"""
|
|
101
|
+
result = parse_recurrence("weekly:mon,fri")
|
|
102
|
+
self.assertEqual(result["pattern"]["type"], "weekly")
|
|
103
|
+
self.assertEqual(result["pattern"]["interval"], 1)
|
|
104
|
+
self.assertEqual(result["pattern"]["daysOfWeek"], ["monday", "friday"])
|
|
105
|
+
|
|
106
|
+
def test_every_n_weeks_with_days(self):
|
|
107
|
+
"""Test 'every 2 weeks:mon,wed,fri' with day specifiers"""
|
|
108
|
+
result = parse_recurrence("every 2 weeks:mon,wed,fri")
|
|
109
|
+
self.assertEqual(result["pattern"]["type"], "weekly")
|
|
110
|
+
self.assertEqual(result["pattern"]["interval"], 2)
|
|
111
|
+
self.assertEqual(
|
|
112
|
+
result["pattern"]["daysOfWeek"], ["monday", "wednesday", "friday"]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def test_case_insensitive(self):
|
|
116
|
+
"""Test that input is case insensitive"""
|
|
117
|
+
result = parse_recurrence("Daily")
|
|
118
|
+
self.assertEqual(result["pattern"]["type"], "daily")
|
|
119
|
+
|
|
120
|
+
result = parse_recurrence("WEEKLY:MON,FRI")
|
|
121
|
+
self.assertEqual(result["pattern"]["daysOfWeek"], ["monday", "friday"])
|
|
122
|
+
|
|
123
|
+
def test_whitespace_handling(self):
|
|
124
|
+
"""Test that extra whitespace is handled"""
|
|
125
|
+
result = parse_recurrence(" daily ")
|
|
126
|
+
self.assertEqual(result["pattern"]["type"], "daily")
|
|
127
|
+
|
|
128
|
+
def test_invalid_expression(self):
|
|
129
|
+
"""Test that invalid expressions raise InvalidRecurrenceExpression"""
|
|
130
|
+
with self.assertRaises(InvalidRecurrenceExpression):
|
|
131
|
+
parse_recurrence("biweekly")
|
|
132
|
+
|
|
133
|
+
def test_invalid_day_abbreviation(self):
|
|
134
|
+
"""Test that invalid day abbreviations raise InvalidRecurrenceExpression"""
|
|
135
|
+
with self.assertRaises(InvalidRecurrenceExpression):
|
|
136
|
+
parse_recurrence("weekly:monday,fri")
|
|
137
|
+
|
|
138
|
+
def test_range_always_no_end(self):
|
|
139
|
+
"""Test that range is always noEnd with today's date"""
|
|
140
|
+
for expr in ["daily", "weekly", "monthly", "yearly"]:
|
|
141
|
+
result = parse_recurrence(expr)
|
|
142
|
+
self.assertEqual(result["range"]["type"], "noEnd")
|
|
143
|
+
self.assertEqual(result["range"]["startDate"], "2026-02-04")
|
|
144
|
+
|
|
145
|
+
def test_singular_unit(self):
|
|
146
|
+
"""Test singular unit forms work"""
|
|
147
|
+
result = parse_recurrence("every 1 day")
|
|
148
|
+
self.assertEqual(result["pattern"]["type"], "daily")
|
|
149
|
+
self.assertEqual(result["pattern"]["interval"], 1)
|
|
150
|
+
|
|
151
|
+
result = parse_recurrence("every 1 week")
|
|
152
|
+
self.assertEqual(result["pattern"]["type"], "weekly")
|
|
153
|
+
|
|
154
|
+
result = parse_recurrence("every 1 month")
|
|
155
|
+
self.assertEqual(result["pattern"]["type"], "absoluteMonthly")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
unittest.main()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for the update command (CLI parsing + wrapper)"""
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch, MagicMock
|
|
6
|
+
from todocli.cli import setup_parser
|
|
7
|
+
from todocli.graphapi.wrapper import update_task
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestUpdateCLIParsing(unittest.TestCase):
|
|
11
|
+
"""Test argparse setup for the update command"""
|
|
12
|
+
|
|
13
|
+
def setUp(self):
|
|
14
|
+
self.parser = setup_parser()
|
|
15
|
+
|
|
16
|
+
def test_update_basic(self):
|
|
17
|
+
args = self.parser.parse_args(["update", "Tasks/my task"])
|
|
18
|
+
self.assertEqual(args.task_name, "Tasks/my task")
|
|
19
|
+
self.assertIsNone(args.title)
|
|
20
|
+
self.assertIsNone(args.due)
|
|
21
|
+
self.assertIsNone(args.reminder)
|
|
22
|
+
self.assertFalse(args.important)
|
|
23
|
+
self.assertIsNone(args.recurrence)
|
|
24
|
+
self.assertIsNone(getattr(args, "list", None))
|
|
25
|
+
|
|
26
|
+
def test_update_with_title(self):
|
|
27
|
+
args = self.parser.parse_args(["update", "Tasks/old", "--title", "new name"])
|
|
28
|
+
self.assertEqual(args.title, "new name")
|
|
29
|
+
|
|
30
|
+
def test_update_with_due(self):
|
|
31
|
+
args = self.parser.parse_args(["update", "Tasks/t", "-d", "15.02.2026"])
|
|
32
|
+
self.assertEqual(args.due, "15.02.2026")
|
|
33
|
+
|
|
34
|
+
def test_update_with_all_flags(self):
|
|
35
|
+
args = self.parser.parse_args(
|
|
36
|
+
[
|
|
37
|
+
"update",
|
|
38
|
+
"Tasks/t",
|
|
39
|
+
"--title",
|
|
40
|
+
"renamed",
|
|
41
|
+
"-d",
|
|
42
|
+
"20.02.2026",
|
|
43
|
+
"-r",
|
|
44
|
+
"9:00",
|
|
45
|
+
"-I",
|
|
46
|
+
"-R",
|
|
47
|
+
"daily",
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
self.assertEqual(args.title, "renamed")
|
|
51
|
+
self.assertEqual(args.due, "20.02.2026")
|
|
52
|
+
self.assertEqual(args.reminder, "9:00")
|
|
53
|
+
self.assertTrue(args.important)
|
|
54
|
+
self.assertEqual(args.recurrence, "daily")
|
|
55
|
+
|
|
56
|
+
def test_update_with_list_flag(self):
|
|
57
|
+
args = self.parser.parse_args(
|
|
58
|
+
["update", "my task", "-l", "Work", "--title", "x"]
|
|
59
|
+
)
|
|
60
|
+
self.assertEqual(args.task_name, "my task")
|
|
61
|
+
self.assertEqual(args.list, "Work")
|
|
62
|
+
self.assertEqual(args.title, "x")
|
|
63
|
+
|
|
64
|
+
def test_update_mutually_exclusive_due(self):
|
|
65
|
+
"""Test --due and --clear-due are mutually exclusive"""
|
|
66
|
+
with self.assertRaises(SystemExit):
|
|
67
|
+
self.parser.parse_args(["update", "t", "-d", "tomorrow", "--clear-due"])
|
|
68
|
+
|
|
69
|
+
def test_update_mutually_exclusive_reminder(self):
|
|
70
|
+
"""Test --reminder and --clear-reminder are mutually exclusive"""
|
|
71
|
+
with self.assertRaises(SystemExit):
|
|
72
|
+
self.parser.parse_args(["update", "t", "-r", "9am", "--clear-reminder"])
|
|
73
|
+
|
|
74
|
+
def test_update_mutually_exclusive_important(self):
|
|
75
|
+
"""Test --important and --no-important are mutually exclusive"""
|
|
76
|
+
with self.assertRaises(SystemExit):
|
|
77
|
+
self.parser.parse_args(["update", "t", "-I", "--no-important"])
|
|
78
|
+
|
|
79
|
+
def test_update_mutually_exclusive_recurrence(self):
|
|
80
|
+
"""Test --recurrence and --clear-recurrence are mutually exclusive"""
|
|
81
|
+
with self.assertRaises(SystemExit):
|
|
82
|
+
self.parser.parse_args(["update", "t", "-R", "daily", "--clear-recurrence"])
|
|
83
|
+
|
|
84
|
+
def test_update_clear_due_alone(self):
|
|
85
|
+
"""Test --clear-due works alone"""
|
|
86
|
+
args = self.parser.parse_args(["update", "t", "--clear-due"])
|
|
87
|
+
self.assertTrue(args.clear_due)
|
|
88
|
+
self.assertIsNone(args.due)
|
|
89
|
+
|
|
90
|
+
def test_update_no_important_alone(self):
|
|
91
|
+
"""Test --no-important works alone"""
|
|
92
|
+
args = self.parser.parse_args(["update", "t", "--no-important"])
|
|
93
|
+
self.assertTrue(args.no_important)
|
|
94
|
+
self.assertFalse(args.important)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestUpdateWrapper(unittest.TestCase):
|
|
98
|
+
"""Test update_task wrapper function with mocked API"""
|
|
99
|
+
|
|
100
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
101
|
+
@patch("todocli.graphapi.wrapper.get_task_id_by_name")
|
|
102
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
103
|
+
def test_update_task_title_only(self, mock_list_id, mock_task_id, mock_session):
|
|
104
|
+
mock_list_id.return_value = "lid"
|
|
105
|
+
mock_task_id.return_value = "tid"
|
|
106
|
+
mock_resp = MagicMock()
|
|
107
|
+
mock_resp.ok = True
|
|
108
|
+
mock_resp.content = b'{"id": "tid", "title": "new title"}'
|
|
109
|
+
mock_session.return_value.patch.return_value = mock_resp
|
|
110
|
+
|
|
111
|
+
task_id, task_title = update_task("Tasks", "my task", title="new title")
|
|
112
|
+
self.assertEqual(task_id, "tid")
|
|
113
|
+
self.assertEqual(task_title, "new title")
|
|
114
|
+
call_kwargs = mock_session.return_value.patch.call_args
|
|
115
|
+
body = call_kwargs.kwargs["json"]
|
|
116
|
+
self.assertEqual(body["title"], "new title")
|
|
117
|
+
self.assertNotIn("dueDateTime", body)
|
|
118
|
+
|
|
119
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
120
|
+
@patch("todocli.graphapi.wrapper.get_task_id_by_name")
|
|
121
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
122
|
+
def test_update_task_due_only(self, mock_list_id, mock_task_id, mock_session):
|
|
123
|
+
from datetime import datetime
|
|
124
|
+
|
|
125
|
+
mock_list_id.return_value = "lid"
|
|
126
|
+
mock_task_id.return_value = "tid"
|
|
127
|
+
mock_resp = MagicMock()
|
|
128
|
+
mock_resp.ok = True
|
|
129
|
+
mock_resp.content = b'{"id": "tid", "title": "my task"}'
|
|
130
|
+
mock_session.return_value.patch.return_value = mock_resp
|
|
131
|
+
|
|
132
|
+
dt = datetime(2026, 2, 15, 7, 0, 0)
|
|
133
|
+
task_id, task_title = update_task("Tasks", "my task", due_datetime=dt)
|
|
134
|
+
self.assertEqual(task_id, "tid")
|
|
135
|
+
body = mock_session.return_value.patch.call_args.kwargs["json"]
|
|
136
|
+
self.assertIn("dueDateTime", body)
|
|
137
|
+
self.assertNotIn("title", body)
|
|
138
|
+
|
|
139
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
140
|
+
@patch("todocli.graphapi.wrapper.get_task_id_by_name")
|
|
141
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
142
|
+
def test_update_task_importance(self, mock_list_id, mock_task_id, mock_session):
|
|
143
|
+
mock_list_id.return_value = "lid"
|
|
144
|
+
mock_task_id.return_value = "tid"
|
|
145
|
+
mock_resp = MagicMock()
|
|
146
|
+
mock_resp.ok = True
|
|
147
|
+
mock_resp.content = b'{"id": "tid", "title": "my task"}'
|
|
148
|
+
mock_session.return_value.patch.return_value = mock_resp
|
|
149
|
+
|
|
150
|
+
task_id, task_title = update_task("Tasks", "my task", important=True)
|
|
151
|
+
self.assertEqual(task_id, "tid")
|
|
152
|
+
body = mock_session.return_value.patch.call_args.kwargs["json"]
|
|
153
|
+
self.assertEqual(body["importance"], "high")
|
|
154
|
+
|
|
155
|
+
@patch("todocli.graphapi.wrapper.get_oauth_session")
|
|
156
|
+
@patch("todocli.graphapi.wrapper.get_task_id_by_name")
|
|
157
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
158
|
+
def test_update_task_multiple_fields(
|
|
159
|
+
self, mock_list_id, mock_task_id, mock_session
|
|
160
|
+
):
|
|
161
|
+
from datetime import datetime
|
|
162
|
+
|
|
163
|
+
mock_list_id.return_value = "lid"
|
|
164
|
+
mock_task_id.return_value = "tid"
|
|
165
|
+
mock_resp = MagicMock()
|
|
166
|
+
mock_resp.ok = True
|
|
167
|
+
mock_resp.content = b'{"id": "tid", "title": "new"}'
|
|
168
|
+
mock_session.return_value.patch.return_value = mock_resp
|
|
169
|
+
|
|
170
|
+
dt = datetime(2026, 3, 1, 7, 0, 0)
|
|
171
|
+
task_id, task_title = update_task(
|
|
172
|
+
"Tasks", "t", title="new", due_datetime=dt, important=True
|
|
173
|
+
)
|
|
174
|
+
self.assertEqual(task_id, "tid")
|
|
175
|
+
self.assertEqual(task_title, "new")
|
|
176
|
+
body = mock_session.return_value.patch.call_args.kwargs["json"]
|
|
177
|
+
self.assertEqual(body["title"], "new")
|
|
178
|
+
self.assertIn("dueDateTime", body)
|
|
179
|
+
self.assertEqual(body["importance"], "high")
|
|
180
|
+
|
|
181
|
+
@patch("todocli.graphapi.wrapper.get_task_id_by_name")
|
|
182
|
+
@patch("todocli.graphapi.wrapper.get_list_id_by_name")
|
|
183
|
+
def test_update_task_empty_body_raises(self, mock_list_id, mock_task_id):
|
|
184
|
+
mock_list_id.return_value = "lid"
|
|
185
|
+
mock_task_id.return_value = "tid"
|
|
186
|
+
|
|
187
|
+
with self.assertRaises(ValueError):
|
|
188
|
+
update_task("Tasks", "t")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
unittest.main()
|