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.
tests/test_utils.py ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ """Shared test utilities and mock factories for todocli tests."""
3
+
4
+ from unittest.mock import MagicMock
5
+ from datetime import datetime
6
+
7
+
8
+ def make_mock_task(
9
+ title: str,
10
+ task_id: str = "tid",
11
+ importance: str = "normal",
12
+ status: str = "notStarted",
13
+ due_datetime: datetime = None,
14
+ reminder_datetime: datetime = None,
15
+ created_datetime: datetime = None,
16
+ ):
17
+ """Create a mock Task object for testing.
18
+
19
+ Args:
20
+ title: Task title
21
+ task_id: Task ID (default: "tid")
22
+ importance: "low", "normal", or "high" (default: "normal")
23
+ status: Task status (default: "notStarted")
24
+ due_datetime: Optional due date
25
+ reminder_datetime: Optional reminder datetime
26
+ created_datetime: Creation datetime (default: 2026-01-01 10:00)
27
+
28
+ Returns:
29
+ MagicMock configured as a Task
30
+ """
31
+ task = MagicMock()
32
+ task.id = task_id
33
+ task.title = title
34
+
35
+ # Mock enum-like importance with .value attribute
36
+ task.importance = MagicMock()
37
+ task.importance.value = importance
38
+
39
+ # Mock enum-like status with .value attribute
40
+ task.status = MagicMock()
41
+ task.status.value = status
42
+
43
+ task.due_datetime = due_datetime
44
+ task.reminder_datetime = reminder_datetime
45
+ task.created_datetime = created_datetime or datetime(2026, 1, 1, 10, 0, 0)
46
+ task.completed_datetime = None
47
+ task.is_reminder_on = reminder_datetime is not None
48
+ task.last_modified_datetime = task.created_datetime
49
+
50
+ # Mock to_dict method
51
+ task.to_dict.return_value = {
52
+ "id": task_id,
53
+ "title": title,
54
+ "status": status,
55
+ "importance": importance,
56
+ "due_datetime": due_datetime.isoformat() if due_datetime else None,
57
+ "reminder_datetime": (
58
+ reminder_datetime.isoformat() if reminder_datetime else None
59
+ ),
60
+ "created_datetime": task.created_datetime.isoformat(),
61
+ "completed_datetime": None,
62
+ "is_reminder_on": task.is_reminder_on,
63
+ "last_modified_datetime": task.created_datetime.isoformat(),
64
+ }
65
+
66
+ return task
67
+
68
+
69
+ def make_mock_list(
70
+ display_name: str,
71
+ list_id: str = "lid",
72
+ is_owner: bool = True,
73
+ is_shared: bool = False,
74
+ ):
75
+ """Create a mock TodoList object for testing.
76
+
77
+ Args:
78
+ display_name: List name
79
+ list_id: List ID (default: "lid")
80
+ is_owner: Whether user owns the list (default: True)
81
+ is_shared: Whether list is shared (default: False)
82
+
83
+ Returns:
84
+ MagicMock configured as a TodoList
85
+ """
86
+ lst = MagicMock()
87
+ lst.id = list_id
88
+ lst.display_name = display_name
89
+ lst.is_owner = is_owner
90
+ lst.is_shared = is_shared
91
+ lst.well_known_list_name = MagicMock()
92
+ lst.well_known_list_name.value = "none"
93
+
94
+ lst.to_dict.return_value = {
95
+ "id": list_id,
96
+ "display_name": display_name,
97
+ "is_owner": is_owner,
98
+ "is_shared": is_shared,
99
+ "well_known_list_name": "none",
100
+ }
101
+
102
+ return lst
103
+
104
+
105
+ def make_mock_step(
106
+ name: str,
107
+ step_id: str = "sid",
108
+ is_checked: bool = False,
109
+ created_datetime: datetime = None,
110
+ ):
111
+ """Create a mock ChecklistItem object for testing.
112
+
113
+ Args:
114
+ name: Step/checklist item name
115
+ step_id: Step ID (default: "sid")
116
+ is_checked: Whether step is completed (default: False)
117
+ created_datetime: Creation datetime (default: 2026-01-01 10:00)
118
+
119
+ Returns:
120
+ MagicMock configured as a ChecklistItem
121
+ """
122
+ step = MagicMock()
123
+ step.id = step_id
124
+ step.display_name = name
125
+ step.is_checked = is_checked
126
+ step.created_datetime = created_datetime or datetime(2026, 1, 1, 10, 0, 0)
127
+ step.checked_datetime = None
128
+
129
+ step.to_dict.return_value = {
130
+ "id": step_id,
131
+ "display_name": name,
132
+ "is_checked": is_checked,
133
+ "created_datetime": step.created_datetime.isoformat(),
134
+ "checked_datetime": None,
135
+ }
136
+
137
+ return step
138
+
139
+
140
+ def make_mock_args(**kwargs):
141
+ """Create mock argparse args with sensible defaults.
142
+
143
+ Commonly used defaults:
144
+ - json: False
145
+ - list_name: "Tasks"
146
+ - no_steps: False
147
+ - date_format: "eu"
148
+ - due_today: False
149
+ - overdue: False
150
+ - important: False
151
+ - list: None
152
+ - task_names: None
153
+ - yes: False
154
+
155
+ Args:
156
+ **kwargs: Override any default values
157
+
158
+ Returns:
159
+ MagicMock configured as argparse Namespace
160
+ """
161
+ args = MagicMock()
162
+
163
+ defaults = {
164
+ "json": False,
165
+ "list_name": "Tasks",
166
+ "no_steps": False,
167
+ "date_format": "eu",
168
+ "due_today": False,
169
+ "overdue": False,
170
+ "important": False,
171
+ "list": None,
172
+ "task_names": None,
173
+ "yes": False,
174
+ "reminder": None,
175
+ "due": None,
176
+ "recurrence": None,
177
+ "title": None,
178
+ }
179
+
180
+ for key, value in defaults.items():
181
+ setattr(args, key, value)
182
+
183
+ for key, value in kwargs.items():
184
+ setattr(args, key, value)
185
+
186
+ return args
tests/test_wrapper.py ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """Unit tests for graph API wrapper module"""
3
+
4
+ import unittest
5
+ from unittest.mock import patch, MagicMock
6
+ import json
7
+ from todocli.graphapi.wrapper import (
8
+ ListNotFound,
9
+ TaskNotFoundByName,
10
+ TaskNotFoundByIndex,
11
+ StepNotFoundByIndex,
12
+ StepNotFoundByName,
13
+ BASE_URL,
14
+ BATCH_URL,
15
+ BATCH_MAX_REQUESTS,
16
+ get_task_id_by_name,
17
+ get_step_id,
18
+ get_checklist_items_batch,
19
+ )
20
+
21
+
22
+ class TestWrapperExceptions(unittest.TestCase):
23
+ """Test custom exception classes"""
24
+
25
+ def test_list_not_found_exception(self):
26
+ """Test ListNotFound exception message"""
27
+ list_name = "NonExistentList"
28
+ exc = ListNotFound(list_name)
29
+
30
+ self.assertIn(list_name, exc.message)
31
+ self.assertIn("could not be found", exc.message)
32
+
33
+ def test_task_not_found_by_name_exception(self):
34
+ """Test TaskNotFoundByName exception message"""
35
+ task_name = "NonExistentTask"
36
+ list_name = "Personal"
37
+ exc = TaskNotFoundByName(task_name, list_name)
38
+
39
+ self.assertIn(task_name, exc.message)
40
+ self.assertIn(list_name, exc.message)
41
+ self.assertIn("could not be found", exc.message)
42
+
43
+ def test_task_not_found_by_index_exception(self):
44
+ """Test TaskNotFoundByIndex exception message"""
45
+ task_index = 999
46
+ list_name = "Personal"
47
+ exc = TaskNotFoundByIndex(task_index, list_name)
48
+
49
+ self.assertIn(str(task_index), exc.message)
50
+ self.assertIn(list_name, exc.message)
51
+ self.assertIn("could not be found", exc.message)
52
+
53
+
54
+ class TestWrapperConstants(unittest.TestCase):
55
+ """Test API endpoint URL constants"""
56
+
57
+ def test_base_url_format(self):
58
+ """Test BASE_URL is correctly formatted"""
59
+ self.assertTrue(BASE_URL.startswith("https://graph.microsoft.com"))
60
+ self.assertIn("/me/todo/lists", BASE_URL)
61
+
62
+ def test_batch_url_format(self):
63
+ """Test BATCH_URL is correctly formatted"""
64
+ self.assertTrue(BATCH_URL.startswith("https://graph.microsoft.com"))
65
+ self.assertIn("/$batch", BATCH_URL)
66
+
67
+
68
+ class TestGetTaskIdByName(unittest.TestCase):
69
+ """Test get_task_id_by_name with int index and invalid types"""
70
+
71
+ @patch("todocli.graphapi.wrapper.get_tasks")
72
+ @patch("todocli.graphapi.wrapper.get_list_id_by_name")
73
+ def test_get_task_id_by_name_with_int_index(self, mock_get_list_id, mock_get_tasks):
74
+ mock_get_list_id.return_value = "list-id-123"
75
+ task0 = MagicMock()
76
+ task0.id = "task-id-0"
77
+ task1 = MagicMock()
78
+ task1.id = "task-id-1"
79
+ mock_get_tasks.return_value = [task0, task1]
80
+
81
+ result = get_task_id_by_name("Tasks", 1)
82
+ self.assertEqual(result, "task-id-1")
83
+ mock_get_tasks.assert_called_once_with(list_name="Tasks")
84
+
85
+ @patch("todocli.graphapi.wrapper.get_tasks")
86
+ @patch("todocli.graphapi.wrapper.get_list_id_by_name")
87
+ def test_get_task_id_by_name_with_invalid_index(
88
+ self, mock_get_list_id, mock_get_tasks
89
+ ):
90
+ mock_get_list_id.return_value = "list-id-123"
91
+ mock_get_tasks.return_value = []
92
+
93
+ with self.assertRaises(TaskNotFoundByIndex):
94
+ get_task_id_by_name("Tasks", 5)
95
+
96
+ def test_get_task_id_by_name_with_invalid_type(self):
97
+ with self.assertRaises(TypeError):
98
+ get_task_id_by_name("Tasks", 3.14)
99
+
100
+
101
+ class TestGetStepId(unittest.TestCase):
102
+ """Test get_step_id with invalid types"""
103
+
104
+ @patch("todocli.graphapi.wrapper.get_checklist_items")
105
+ def test_get_step_id_with_invalid_type(self, mock_get_items):
106
+ with self.assertRaises(TypeError):
107
+ get_step_id("Tasks", "my task", 3.14, list_id="lid", task_id="tid")
108
+
109
+
110
+ class TestGetChecklistItemsBatch(unittest.TestCase):
111
+ """Test get_checklist_items_batch using $batch API"""
112
+
113
+ @patch("todocli.graphapi.wrapper.get_oauth_session")
114
+ def test_batch_single_task(self, mock_session):
115
+ batch_response = {
116
+ "responses": [
117
+ {
118
+ "id": "tid-1",
119
+ "status": 200,
120
+ "body": {
121
+ "value": [
122
+ {
123
+ "id": "step-1",
124
+ "displayName": "Buy eggs",
125
+ "isChecked": False,
126
+ "checkedDateTime": None,
127
+ "createdDateTime": "2026-01-01T00:00:00.0000000Z",
128
+ }
129
+ ]
130
+ },
131
+ }
132
+ ]
133
+ }
134
+ mock_resp = MagicMock()
135
+ mock_resp.ok = True
136
+ mock_resp.content = json.dumps(batch_response).encode()
137
+ mock_session.return_value.post.return_value = mock_resp
138
+
139
+ result = get_checklist_items_batch("lid-1", ["tid-1"])
140
+ self.assertIn("tid-1", result)
141
+ self.assertEqual(len(result["tid-1"]), 1)
142
+ self.assertEqual(result["tid-1"][0].display_name, "Buy eggs")
143
+
144
+ # Verify batch request format
145
+ call_args = mock_session.return_value.post.call_args
146
+ self.assertEqual(call_args.args[0], BATCH_URL)
147
+ req_body = call_args.kwargs["json"]
148
+ self.assertEqual(len(req_body["requests"]), 1)
149
+ self.assertEqual(req_body["requests"][0]["method"], "GET")
150
+
151
+ def test_batch_empty_tasks(self):
152
+ result = get_checklist_items_batch("lid-1", [])
153
+ self.assertEqual(result, {})
154
+
155
+ @patch("todocli.graphapi.wrapper.get_oauth_session")
156
+ def test_batch_chunking(self, mock_session):
157
+ task_ids = [f"tid-{i}" for i in range(25)]
158
+
159
+ def make_batch_response(chunk_ids):
160
+ return {
161
+ "responses": [
162
+ {"id": tid, "status": 200, "body": {"value": []}}
163
+ for tid in chunk_ids
164
+ ]
165
+ }
166
+
167
+ call_count = [0]
168
+
169
+ def mock_post(url, json=None):
170
+ resp = MagicMock()
171
+ resp.ok = True
172
+ chunk_ids = [r["id"] for r in json["requests"]]
173
+ resp.content = (
174
+ __import__("json").dumps(make_batch_response(chunk_ids)).encode()
175
+ )
176
+ call_count[0] += 1
177
+ return resp
178
+
179
+ mock_session.return_value.post.side_effect = mock_post
180
+
181
+ result = get_checklist_items_batch("lid-1", task_ids)
182
+
183
+ # Should have made 2 calls: 20 + 5
184
+ self.assertEqual(call_count[0], 2)
185
+ self.assertEqual(len(result), 25)
186
+ for tid in task_ids:
187
+ self.assertIn(tid, result)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ unittest.main()
todocli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"