iflow-mcp_democratize-technology-chronos-mcp 2.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.
- chronos_mcp/__init__.py +5 -0
- chronos_mcp/__main__.py +9 -0
- chronos_mcp/accounts.py +410 -0
- chronos_mcp/bulk.py +946 -0
- chronos_mcp/caldav_utils.py +149 -0
- chronos_mcp/calendars.py +204 -0
- chronos_mcp/config.py +187 -0
- chronos_mcp/credentials.py +190 -0
- chronos_mcp/events.py +515 -0
- chronos_mcp/exceptions.py +477 -0
- chronos_mcp/journals.py +477 -0
- chronos_mcp/logging_config.py +23 -0
- chronos_mcp/models.py +202 -0
- chronos_mcp/py.typed +0 -0
- chronos_mcp/rrule.py +259 -0
- chronos_mcp/search.py +315 -0
- chronos_mcp/server.py +121 -0
- chronos_mcp/tasks.py +518 -0
- chronos_mcp/tools/__init__.py +29 -0
- chronos_mcp/tools/accounts.py +151 -0
- chronos_mcp/tools/base.py +59 -0
- chronos_mcp/tools/bulk.py +557 -0
- chronos_mcp/tools/calendars.py +142 -0
- chronos_mcp/tools/events.py +698 -0
- chronos_mcp/tools/journals.py +310 -0
- chronos_mcp/tools/tasks.py +414 -0
- chronos_mcp/utils.py +163 -0
- chronos_mcp/validation.py +636 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +91 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_accounts.py +380 -0
- tests/unit/test_accounts_ssrf.py +134 -0
- tests/unit/test_base.py +135 -0
- tests/unit/test_bulk.py +380 -0
- tests/unit/test_bulk_create.py +408 -0
- tests/unit/test_bulk_delete.py +341 -0
- tests/unit/test_bulk_resource_limits.py +74 -0
- tests/unit/test_caldav_utils.py +300 -0
- tests/unit/test_calendars.py +286 -0
- tests/unit/test_config.py +111 -0
- tests/unit/test_config_validation.py +128 -0
- tests/unit/test_credentials_security.py +189 -0
- tests/unit/test_cryptography_security.py +178 -0
- tests/unit/test_events.py +536 -0
- tests/unit/test_exceptions.py +58 -0
- tests/unit/test_journals.py +1097 -0
- tests/unit/test_models.py +95 -0
- tests/unit/test_race_conditions.py +202 -0
- tests/unit/test_recurring_events.py +156 -0
- tests/unit/test_rrule.py +217 -0
- tests/unit/test_search.py +372 -0
- tests/unit/test_search_advanced.py +333 -0
- tests/unit/test_server_input_validation.py +219 -0
- tests/unit/test_ssrf_protection.py +505 -0
- tests/unit/test_tasks.py +918 -0
- tests/unit/test_thread_safety.py +301 -0
- tests/unit/test_tools_journals.py +617 -0
- tests/unit/test_tools_tasks.py +968 -0
- tests/unit/test_url_validation_security.py +234 -0
- tests/unit/test_utils.py +180 -0
- tests/unit/test_validation.py +983 -0
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive unit tests for journal management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from icalendar import Calendar as iCalendar
|
|
12
|
+
from icalendar import Event as iEvent
|
|
13
|
+
from icalendar import Journal as iJournal
|
|
14
|
+
|
|
15
|
+
from chronos_mcp.calendars import CalendarManager
|
|
16
|
+
from chronos_mcp.exceptions import (
|
|
17
|
+
CalendarNotFoundError,
|
|
18
|
+
ChronosError,
|
|
19
|
+
EventCreationError,
|
|
20
|
+
EventDeletionError,
|
|
21
|
+
JournalNotFoundError,
|
|
22
|
+
)
|
|
23
|
+
from chronos_mcp.journals import JournalManager
|
|
24
|
+
from chronos_mcp.models import Journal
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestJournalManagerInit:
|
|
28
|
+
"""Test JournalManager initialization and basic functionality"""
|
|
29
|
+
|
|
30
|
+
def test_init_with_calendar_manager(self):
|
|
31
|
+
"""Test JournalManager initialization with CalendarManager"""
|
|
32
|
+
mock_calendar_manager = Mock(spec=CalendarManager)
|
|
33
|
+
journal_manager = JournalManager(mock_calendar_manager)
|
|
34
|
+
assert journal_manager.calendars == mock_calendar_manager
|
|
35
|
+
|
|
36
|
+
def test_get_default_account_success(self):
|
|
37
|
+
"""Test _get_default_account returns account when available"""
|
|
38
|
+
mock_calendar_manager = Mock()
|
|
39
|
+
mock_calendar_manager.accounts = Mock()
|
|
40
|
+
mock_calendar_manager.accounts.config = Mock()
|
|
41
|
+
mock_calendar_manager.accounts.config.config = Mock()
|
|
42
|
+
mock_calendar_manager.accounts.config.config.default_account = "test_account"
|
|
43
|
+
|
|
44
|
+
journal_manager = JournalManager(mock_calendar_manager)
|
|
45
|
+
result = journal_manager._get_default_account()
|
|
46
|
+
assert result == "test_account"
|
|
47
|
+
|
|
48
|
+
def test_get_default_account_exception(self):
|
|
49
|
+
"""Test _get_default_account returns None on exception"""
|
|
50
|
+
mock_calendar_manager = Mock()
|
|
51
|
+
# Remove the accounts attribute to force AttributeError
|
|
52
|
+
del mock_calendar_manager.accounts
|
|
53
|
+
|
|
54
|
+
journal_manager = JournalManager(mock_calendar_manager)
|
|
55
|
+
result = journal_manager._get_default_account()
|
|
56
|
+
assert result is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestJournalCRUD:
|
|
60
|
+
"""Test CRUD operations for journals"""
|
|
61
|
+
|
|
62
|
+
@pytest.fixture
|
|
63
|
+
def mock_calendar_manager(self):
|
|
64
|
+
"""Mock CalendarManager for testing"""
|
|
65
|
+
return Mock(spec=CalendarManager)
|
|
66
|
+
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def mock_calendar(self):
|
|
69
|
+
"""Mock calendar object with journal support"""
|
|
70
|
+
calendar = Mock()
|
|
71
|
+
calendar.save_journal = Mock()
|
|
72
|
+
calendar.save_event = Mock()
|
|
73
|
+
calendar.journals = Mock()
|
|
74
|
+
calendar.events = Mock()
|
|
75
|
+
calendar.event_by_uid = Mock()
|
|
76
|
+
return calendar
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def journal_manager(self, mock_calendar_manager):
|
|
80
|
+
"""JournalManager instance for testing"""
|
|
81
|
+
return JournalManager(mock_calendar_manager)
|
|
82
|
+
|
|
83
|
+
@pytest.fixture
|
|
84
|
+
def sample_journal_data(self):
|
|
85
|
+
"""Sample journal data for testing"""
|
|
86
|
+
return {
|
|
87
|
+
"calendar_uid": "cal-123",
|
|
88
|
+
"summary": "Daily Reflection",
|
|
89
|
+
"description": "Today was productive",
|
|
90
|
+
"dtstart": datetime(2025, 7, 10, 9, 0, tzinfo=timezone.utc),
|
|
91
|
+
"related_to": ["event-456"],
|
|
92
|
+
"account_alias": "test_account",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def test_create_journal_success_with_save_journal(
|
|
96
|
+
self, journal_manager, mock_calendar, sample_journal_data
|
|
97
|
+
):
|
|
98
|
+
"""Test successful journal creation using save_journal method"""
|
|
99
|
+
# Setup
|
|
100
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
101
|
+
mock_caldav_journal = Mock()
|
|
102
|
+
mock_calendar.save_journal.return_value = mock_caldav_journal
|
|
103
|
+
|
|
104
|
+
with patch("uuid.uuid4", return_value="test-uid-123"):
|
|
105
|
+
result = journal_manager.create_journal(**sample_journal_data)
|
|
106
|
+
|
|
107
|
+
# Assertions
|
|
108
|
+
assert result is not None
|
|
109
|
+
assert result.uid == "test-uid-123"
|
|
110
|
+
assert result.summary == "Daily Reflection"
|
|
111
|
+
assert result.description == "Today was productive"
|
|
112
|
+
mock_calendar.save_journal.assert_called_once()
|
|
113
|
+
|
|
114
|
+
def test_create_journal_fallback_to_save_event(
|
|
115
|
+
self, journal_manager, mock_calendar, sample_journal_data
|
|
116
|
+
):
|
|
117
|
+
"""Test journal creation falls back to save_event when save_journal fails"""
|
|
118
|
+
# Setup
|
|
119
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
120
|
+
mock_calendar.save_journal.side_effect = Exception("save_journal failed")
|
|
121
|
+
mock_caldav_journal = Mock()
|
|
122
|
+
mock_calendar.save_event.return_value = mock_caldav_journal
|
|
123
|
+
|
|
124
|
+
with patch("uuid.uuid4", return_value="test-uid-123"):
|
|
125
|
+
result = journal_manager.create_journal(**sample_journal_data)
|
|
126
|
+
|
|
127
|
+
# Assertions
|
|
128
|
+
assert result is not None
|
|
129
|
+
assert result.uid == "test-uid-123"
|
|
130
|
+
mock_calendar.save_journal.assert_called_once()
|
|
131
|
+
mock_calendar.save_event.assert_called_once()
|
|
132
|
+
|
|
133
|
+
def test_create_journal_no_save_journal_method(
|
|
134
|
+
self, journal_manager, sample_journal_data
|
|
135
|
+
):
|
|
136
|
+
"""Test journal creation when calendar doesn't have save_journal method"""
|
|
137
|
+
# Setup calendar without save_journal method
|
|
138
|
+
mock_calendar = Mock()
|
|
139
|
+
mock_calendar.save_event = Mock()
|
|
140
|
+
# Remove save_journal method
|
|
141
|
+
if hasattr(mock_calendar, "save_journal"):
|
|
142
|
+
delattr(mock_calendar, "save_journal")
|
|
143
|
+
|
|
144
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
145
|
+
mock_caldav_journal = Mock()
|
|
146
|
+
mock_calendar.save_event.return_value = mock_caldav_journal
|
|
147
|
+
|
|
148
|
+
with patch("uuid.uuid4", return_value="test-uid-123"):
|
|
149
|
+
result = journal_manager.create_journal(**sample_journal_data)
|
|
150
|
+
|
|
151
|
+
# Assertions
|
|
152
|
+
assert result is not None
|
|
153
|
+
assert result.uid == "test-uid-123"
|
|
154
|
+
mock_calendar.save_event.assert_called_once()
|
|
155
|
+
|
|
156
|
+
def test_create_journal_minimal_data(self, journal_manager, mock_calendar):
|
|
157
|
+
"""Test journal creation with minimal required data"""
|
|
158
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
159
|
+
mock_caldav_journal = Mock()
|
|
160
|
+
mock_calendar.save_journal.return_value = mock_caldav_journal
|
|
161
|
+
|
|
162
|
+
with patch("uuid.uuid4", return_value="test-uid-123"):
|
|
163
|
+
with patch("chronos_mcp.journals.datetime") as mock_datetime:
|
|
164
|
+
mock_now = datetime(2025, 7, 10, 10, 0, tzinfo=timezone.utc)
|
|
165
|
+
mock_datetime.now.return_value = mock_now
|
|
166
|
+
mock_datetime.timezone = timezone
|
|
167
|
+
|
|
168
|
+
result = journal_manager.create_journal(
|
|
169
|
+
calendar_uid="cal-123", summary="Simple Journal"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert result is not None
|
|
173
|
+
assert result.uid == "test-uid-123"
|
|
174
|
+
assert result.summary == "Simple Journal"
|
|
175
|
+
assert result.description is None
|
|
176
|
+
assert result.related_to == []
|
|
177
|
+
|
|
178
|
+
def test_create_journal_calendar_not_found(self, journal_manager):
|
|
179
|
+
"""Test journal creation with non-existent calendar"""
|
|
180
|
+
journal_manager.calendars.get_calendar.return_value = None
|
|
181
|
+
|
|
182
|
+
with pytest.raises(CalendarNotFoundError):
|
|
183
|
+
journal_manager.create_journal(calendar_uid="nonexistent", summary="Test")
|
|
184
|
+
|
|
185
|
+
def test_create_journal_authorization_error(
|
|
186
|
+
self, journal_manager, mock_calendar, sample_journal_data
|
|
187
|
+
):
|
|
188
|
+
"""Test journal creation with authorization error"""
|
|
189
|
+
from caldav.lib.error import AuthorizationError
|
|
190
|
+
|
|
191
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
192
|
+
# Both save_journal and save_event should fail with authorization error
|
|
193
|
+
mock_calendar.save_journal.side_effect = AuthorizationError("Unauthorized")
|
|
194
|
+
mock_calendar.save_event.side_effect = AuthorizationError("Unauthorized")
|
|
195
|
+
|
|
196
|
+
with pytest.raises(EventCreationError) as exc_info:
|
|
197
|
+
journal_manager.create_journal(**sample_journal_data)
|
|
198
|
+
|
|
199
|
+
assert "Authorization failed" in str(exc_info.value)
|
|
200
|
+
|
|
201
|
+
def test_create_journal_generic_error(
|
|
202
|
+
self, journal_manager, mock_calendar, sample_journal_data
|
|
203
|
+
):
|
|
204
|
+
"""Test journal creation with generic error"""
|
|
205
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
206
|
+
mock_calendar.save_journal.side_effect = Exception("Generic error")
|
|
207
|
+
mock_calendar.save_event.side_effect = Exception("Generic error")
|
|
208
|
+
|
|
209
|
+
with pytest.raises(EventCreationError) as exc_info:
|
|
210
|
+
journal_manager.create_journal(**sample_journal_data)
|
|
211
|
+
|
|
212
|
+
assert "Generic error" in str(exc_info.value)
|
|
213
|
+
|
|
214
|
+
def test_get_journal_success_with_event_by_uid(
|
|
215
|
+
self, journal_manager, mock_calendar
|
|
216
|
+
):
|
|
217
|
+
"""Test successful journal retrieval using event_by_uid"""
|
|
218
|
+
# Setup
|
|
219
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
220
|
+
|
|
221
|
+
# Create mock CalDAV journal with VJOURNAL data
|
|
222
|
+
mock_caldav_journal = Mock()
|
|
223
|
+
mock_caldav_journal.data = self._create_sample_ical_data()
|
|
224
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_journal
|
|
225
|
+
|
|
226
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
227
|
+
expected_journal = Journal(
|
|
228
|
+
uid="test-journal-123",
|
|
229
|
+
summary="Test Journal",
|
|
230
|
+
description="Test Description",
|
|
231
|
+
dtstart=datetime(2025, 7, 10, 9, 0, tzinfo=timezone.utc),
|
|
232
|
+
calendar_uid="cal-123",
|
|
233
|
+
account_alias="test_account",
|
|
234
|
+
)
|
|
235
|
+
mock_parse.return_value = expected_journal
|
|
236
|
+
|
|
237
|
+
result = journal_manager.get_journal(
|
|
238
|
+
journal_uid="test-journal-123",
|
|
239
|
+
calendar_uid="cal-123",
|
|
240
|
+
account_alias="test_account",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
assert result == expected_journal
|
|
244
|
+
mock_calendar.event_by_uid.assert_called_once_with("test-journal-123")
|
|
245
|
+
mock_parse.assert_called_once()
|
|
246
|
+
|
|
247
|
+
def test_get_journal_fallback_to_journals_search(
|
|
248
|
+
self, journal_manager, mock_calendar
|
|
249
|
+
):
|
|
250
|
+
"""Test journal retrieval fallback to searching through journals"""
|
|
251
|
+
# Setup
|
|
252
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
253
|
+
mock_calendar.event_by_uid.side_effect = Exception("event_by_uid failed")
|
|
254
|
+
|
|
255
|
+
# Create proper iCalendar mock data
|
|
256
|
+
mock_journal1 = Mock()
|
|
257
|
+
cal1 = iCalendar()
|
|
258
|
+
journal1 = iJournal()
|
|
259
|
+
journal1.add("uid", "different-uid")
|
|
260
|
+
journal1.add("summary", "Different Journal")
|
|
261
|
+
journal1.add("dtstart", datetime.now(timezone.utc))
|
|
262
|
+
cal1.add_component(journal1)
|
|
263
|
+
mock_journal1.data = cal1.to_ical()
|
|
264
|
+
|
|
265
|
+
mock_journal2 = Mock()
|
|
266
|
+
cal2 = iCalendar()
|
|
267
|
+
journal2 = iJournal()
|
|
268
|
+
journal2.add("uid", "test-journal-123")
|
|
269
|
+
journal2.add("summary", "Found Journal")
|
|
270
|
+
journal2.add("dtstart", datetime.now(timezone.utc))
|
|
271
|
+
cal2.add_component(journal2)
|
|
272
|
+
mock_journal2.data = cal2.to_ical()
|
|
273
|
+
|
|
274
|
+
mock_calendar.journals.return_value = [mock_journal1, mock_journal2]
|
|
275
|
+
|
|
276
|
+
result = journal_manager.get_journal(
|
|
277
|
+
journal_uid="test-journal-123", calendar_uid="cal-123"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
assert result is not None
|
|
281
|
+
assert result.uid == "test-journal-123"
|
|
282
|
+
assert result.summary == "Found Journal"
|
|
283
|
+
mock_calendar.journals.assert_called_once()
|
|
284
|
+
|
|
285
|
+
def test_get_journal_fallback_to_events_search(self, journal_manager):
|
|
286
|
+
"""Test journal retrieval fallback to searching through events when journals() unavailable"""
|
|
287
|
+
# Setup calendar without journals method
|
|
288
|
+
mock_calendar = Mock()
|
|
289
|
+
mock_calendar.event_by_uid.side_effect = Exception("event_by_uid failed")
|
|
290
|
+
mock_calendar.events = Mock()
|
|
291
|
+
# Remove journals method
|
|
292
|
+
if hasattr(mock_calendar, "journals"):
|
|
293
|
+
delattr(mock_calendar, "journals")
|
|
294
|
+
|
|
295
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
296
|
+
|
|
297
|
+
# Create proper iCalendar mock data
|
|
298
|
+
mock_event1 = Mock()
|
|
299
|
+
cal1 = iCalendar()
|
|
300
|
+
event1 = iEvent()
|
|
301
|
+
event1.add("uid", "event-uid")
|
|
302
|
+
event1.add("summary", "Event")
|
|
303
|
+
cal1.add_component(event1)
|
|
304
|
+
mock_event1.data = cal1.to_ical()
|
|
305
|
+
|
|
306
|
+
mock_event2 = Mock()
|
|
307
|
+
cal2 = iCalendar()
|
|
308
|
+
journal2 = iJournal()
|
|
309
|
+
journal2.add("uid", "test-journal-123")
|
|
310
|
+
journal2.add("summary", "Found in Events")
|
|
311
|
+
journal2.add("dtstart", datetime.now(timezone.utc))
|
|
312
|
+
cal2.add_component(journal2)
|
|
313
|
+
mock_event2.data = cal2.to_ical()
|
|
314
|
+
|
|
315
|
+
mock_calendar.events.return_value = [mock_event1, mock_event2]
|
|
316
|
+
|
|
317
|
+
result = journal_manager.get_journal(
|
|
318
|
+
journal_uid="test-journal-123", calendar_uid="cal-123"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
assert result is not None
|
|
322
|
+
assert result.uid == "test-journal-123"
|
|
323
|
+
mock_calendar.events.assert_called_once()
|
|
324
|
+
|
|
325
|
+
def test_get_journal_not_found(self, journal_manager, mock_calendar):
|
|
326
|
+
"""Test journal retrieval when journal doesn't exist"""
|
|
327
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
328
|
+
mock_calendar.event_by_uid.side_effect = Exception("Not found")
|
|
329
|
+
mock_calendar.journals.return_value = []
|
|
330
|
+
|
|
331
|
+
with pytest.raises(JournalNotFoundError):
|
|
332
|
+
journal_manager.get_journal(
|
|
333
|
+
journal_uid="nonexistent", calendar_uid="cal-123"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def test_get_journal_calendar_not_found(self, journal_manager):
|
|
337
|
+
"""Test journal retrieval with non-existent calendar"""
|
|
338
|
+
journal_manager.calendars.get_calendar.return_value = None
|
|
339
|
+
|
|
340
|
+
with pytest.raises(CalendarNotFoundError):
|
|
341
|
+
journal_manager.get_journal(
|
|
342
|
+
journal_uid="test-journal-123", calendar_uid="nonexistent"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def test_get_journal_generic_error(self, journal_manager, mock_calendar):
|
|
346
|
+
"""Test journal retrieval with generic error"""
|
|
347
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
348
|
+
mock_calendar.event_by_uid.side_effect = Exception("Generic error")
|
|
349
|
+
mock_calendar.journals.side_effect = Exception("Generic error")
|
|
350
|
+
|
|
351
|
+
with pytest.raises(ChronosError):
|
|
352
|
+
journal_manager.get_journal(
|
|
353
|
+
journal_uid="test-journal-123", calendar_uid="cal-123"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def test_list_journals_success_with_journals_method(
|
|
357
|
+
self, journal_manager, mock_calendar
|
|
358
|
+
):
|
|
359
|
+
"""Test successful journal listing using journals() method"""
|
|
360
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
361
|
+
|
|
362
|
+
# Mock journals
|
|
363
|
+
mock_journals = [Mock(), Mock(), Mock()]
|
|
364
|
+
mock_calendar.journals.return_value = mock_journals
|
|
365
|
+
|
|
366
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
367
|
+
mock_parse.side_effect = [
|
|
368
|
+
Journal(
|
|
369
|
+
uid="j1",
|
|
370
|
+
summary="Journal 1",
|
|
371
|
+
dtstart=datetime.now(timezone.utc),
|
|
372
|
+
calendar_uid="cal-123",
|
|
373
|
+
account_alias="test",
|
|
374
|
+
),
|
|
375
|
+
None, # One unparseable journal
|
|
376
|
+
Journal(
|
|
377
|
+
uid="j3",
|
|
378
|
+
summary="Journal 3",
|
|
379
|
+
dtstart=datetime.now(timezone.utc),
|
|
380
|
+
calendar_uid="cal-123",
|
|
381
|
+
account_alias="test",
|
|
382
|
+
),
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
result = journal_manager.list_journals(calendar_uid="cal-123")
|
|
386
|
+
|
|
387
|
+
assert len(result) == 2
|
|
388
|
+
assert result[0].uid == "j1"
|
|
389
|
+
assert result[1].uid == "j3"
|
|
390
|
+
mock_calendar.journals.assert_called_once()
|
|
391
|
+
|
|
392
|
+
def test_list_journals_fallback_to_events(self, journal_manager):
|
|
393
|
+
"""Test journal listing fallback to events() when journals() unavailable"""
|
|
394
|
+
# Setup calendar without journals method
|
|
395
|
+
mock_calendar = Mock()
|
|
396
|
+
mock_calendar.events = Mock()
|
|
397
|
+
if hasattr(mock_calendar, "journals"):
|
|
398
|
+
delattr(mock_calendar, "journals")
|
|
399
|
+
|
|
400
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
401
|
+
|
|
402
|
+
# Mock events
|
|
403
|
+
mock_events = [Mock(), Mock()]
|
|
404
|
+
mock_calendar.events.return_value = mock_events
|
|
405
|
+
|
|
406
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
407
|
+
mock_parse.side_effect = [
|
|
408
|
+
Journal(
|
|
409
|
+
uid="j1",
|
|
410
|
+
summary="Journal from Events",
|
|
411
|
+
dtstart=datetime.now(timezone.utc),
|
|
412
|
+
calendar_uid="cal-123",
|
|
413
|
+
account_alias="test",
|
|
414
|
+
),
|
|
415
|
+
None, # One non-journal event
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
result = journal_manager.list_journals(calendar_uid="cal-123")
|
|
419
|
+
|
|
420
|
+
assert len(result) == 1
|
|
421
|
+
assert result[0].uid == "j1"
|
|
422
|
+
mock_calendar.events.assert_called_once()
|
|
423
|
+
|
|
424
|
+
def test_list_journals_with_limit(self, journal_manager, mock_calendar):
|
|
425
|
+
"""Test journal listing with limit parameter"""
|
|
426
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
427
|
+
|
|
428
|
+
# Mock 5 journals
|
|
429
|
+
mock_journals = [Mock() for _ in range(5)]
|
|
430
|
+
mock_calendar.journals.return_value = mock_journals
|
|
431
|
+
|
|
432
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
433
|
+
mock_parse.side_effect = [
|
|
434
|
+
Journal(
|
|
435
|
+
uid=f"j{i}",
|
|
436
|
+
summary=f"Journal {i}",
|
|
437
|
+
dtstart=datetime.now(timezone.utc),
|
|
438
|
+
calendar_uid="cal-123",
|
|
439
|
+
account_alias="test",
|
|
440
|
+
)
|
|
441
|
+
for i in range(5)
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
result = journal_manager.list_journals(calendar_uid="cal-123", limit=3)
|
|
445
|
+
|
|
446
|
+
assert len(result) == 3
|
|
447
|
+
assert result[0].uid == "j0"
|
|
448
|
+
assert result[2].uid == "j2"
|
|
449
|
+
|
|
450
|
+
def test_list_journals_journals_method_fails_retry_with_events(
|
|
451
|
+
self, journal_manager, mock_calendar
|
|
452
|
+
):
|
|
453
|
+
"""Test journal listing when journals() fails but events() succeeds"""
|
|
454
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
455
|
+
mock_calendar.journals.side_effect = Exception("journals() failed")
|
|
456
|
+
|
|
457
|
+
# Mock events for fallback
|
|
458
|
+
mock_events = [Mock()]
|
|
459
|
+
mock_calendar.events.return_value = mock_events
|
|
460
|
+
|
|
461
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
462
|
+
mock_parse.return_value = Journal(
|
|
463
|
+
uid="j1",
|
|
464
|
+
summary="From Events Fallback",
|
|
465
|
+
dtstart=datetime.now(timezone.utc),
|
|
466
|
+
calendar_uid="cal-123",
|
|
467
|
+
account_alias="test",
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
result = journal_manager.list_journals(calendar_uid="cal-123")
|
|
471
|
+
|
|
472
|
+
assert len(result) == 1
|
|
473
|
+
assert result[0].uid == "j1"
|
|
474
|
+
mock_calendar.events.assert_called_once()
|
|
475
|
+
|
|
476
|
+
def test_list_journals_calendar_not_found(self, journal_manager):
|
|
477
|
+
"""Test journal listing with non-existent calendar"""
|
|
478
|
+
journal_manager.calendars.get_calendar.return_value = None
|
|
479
|
+
|
|
480
|
+
with pytest.raises(CalendarNotFoundError):
|
|
481
|
+
journal_manager.list_journals(calendar_uid="nonexistent")
|
|
482
|
+
|
|
483
|
+
def _create_sample_ical_data(self):
|
|
484
|
+
"""Create sample iCalendar data with VJOURNAL"""
|
|
485
|
+
cal = iCalendar()
|
|
486
|
+
journal = iJournal()
|
|
487
|
+
journal.add("uid", "test-journal-123")
|
|
488
|
+
journal.add("summary", "Test Journal")
|
|
489
|
+
journal.add("description", "Test Description")
|
|
490
|
+
journal.add("dtstart", datetime(2025, 7, 10, 9, 0, tzinfo=timezone.utc))
|
|
491
|
+
cal.add_component(journal)
|
|
492
|
+
return cal.to_ical().decode("utf-8")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class TestJournalServerCompatibility:
|
|
496
|
+
"""Test server compatibility and fallback mechanisms"""
|
|
497
|
+
|
|
498
|
+
@pytest.fixture
|
|
499
|
+
def journal_manager(self):
|
|
500
|
+
"""JournalManager instance for testing"""
|
|
501
|
+
mock_calendar_manager = Mock(spec=CalendarManager)
|
|
502
|
+
return JournalManager(mock_calendar_manager)
|
|
503
|
+
|
|
504
|
+
def test_update_journal_success_with_event_by_uid(self, journal_manager):
|
|
505
|
+
"""Test successful journal update using event_by_uid"""
|
|
506
|
+
# Setup
|
|
507
|
+
mock_calendar = Mock()
|
|
508
|
+
mock_calendar.event_by_uid = Mock()
|
|
509
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
510
|
+
|
|
511
|
+
# Create mock existing journal
|
|
512
|
+
existing_ical = self._create_journal_ical()
|
|
513
|
+
mock_caldav_journal = Mock()
|
|
514
|
+
mock_caldav_journal.data = existing_ical
|
|
515
|
+
mock_caldav_journal.save = Mock()
|
|
516
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_journal
|
|
517
|
+
|
|
518
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
519
|
+
updated_journal = Journal(
|
|
520
|
+
uid="test-journal-123",
|
|
521
|
+
summary="Updated Summary",
|
|
522
|
+
description="Updated Description",
|
|
523
|
+
dtstart=datetime(2025, 7, 10, 10, 0, tzinfo=timezone.utc),
|
|
524
|
+
calendar_uid="cal-123",
|
|
525
|
+
account_alias="test",
|
|
526
|
+
)
|
|
527
|
+
mock_parse.return_value = updated_journal
|
|
528
|
+
|
|
529
|
+
result = journal_manager.update_journal(
|
|
530
|
+
journal_uid="test-journal-123",
|
|
531
|
+
calendar_uid="cal-123",
|
|
532
|
+
summary="Updated Summary",
|
|
533
|
+
description="Updated Description",
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
assert result == updated_journal
|
|
537
|
+
mock_caldav_journal.save.assert_called_once()
|
|
538
|
+
|
|
539
|
+
def test_update_journal_fallback_search_with_journals(self, journal_manager):
|
|
540
|
+
"""Test journal update fallback to searching through journals()"""
|
|
541
|
+
# Setup
|
|
542
|
+
mock_calendar = Mock()
|
|
543
|
+
mock_calendar.event_by_uid.side_effect = Exception("event_by_uid failed")
|
|
544
|
+
mock_calendar.journals = Mock()
|
|
545
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
546
|
+
|
|
547
|
+
# Mock journal search
|
|
548
|
+
existing_ical = self._create_journal_ical()
|
|
549
|
+
mock_journal1 = Mock()
|
|
550
|
+
mock_journal1.data = "different journal"
|
|
551
|
+
mock_journal2 = Mock()
|
|
552
|
+
mock_journal2.data = existing_ical
|
|
553
|
+
mock_journal2.save = Mock()
|
|
554
|
+
mock_calendar.journals.return_value = [mock_journal1, mock_journal2]
|
|
555
|
+
|
|
556
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
557
|
+
mock_parse.return_value = Journal(
|
|
558
|
+
uid="test-journal-123",
|
|
559
|
+
summary="Updated via Search",
|
|
560
|
+
dtstart=datetime.now(timezone.utc),
|
|
561
|
+
calendar_uid="cal-123",
|
|
562
|
+
account_alias="test",
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
result = journal_manager.update_journal(
|
|
566
|
+
journal_uid="test-journal-123",
|
|
567
|
+
calendar_uid="cal-123",
|
|
568
|
+
summary="Updated via Search",
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
assert result is not None
|
|
572
|
+
mock_journal2.save.assert_called_once()
|
|
573
|
+
|
|
574
|
+
def test_update_journal_fallback_search_with_events(self, journal_manager):
|
|
575
|
+
"""Test journal update fallback to searching through events()"""
|
|
576
|
+
# Setup calendar without journals method
|
|
577
|
+
mock_calendar = Mock()
|
|
578
|
+
mock_calendar.event_by_uid.side_effect = Exception("event_by_uid failed")
|
|
579
|
+
mock_calendar.events = Mock()
|
|
580
|
+
if hasattr(mock_calendar, "journals"):
|
|
581
|
+
delattr(mock_calendar, "journals")
|
|
582
|
+
|
|
583
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
584
|
+
|
|
585
|
+
# Mock event search
|
|
586
|
+
existing_ical = self._create_journal_ical()
|
|
587
|
+
mock_event = Mock()
|
|
588
|
+
mock_event.data = existing_ical
|
|
589
|
+
mock_event.save = Mock()
|
|
590
|
+
mock_calendar.events.return_value = [mock_event]
|
|
591
|
+
|
|
592
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
593
|
+
mock_parse.return_value = Journal(
|
|
594
|
+
uid="test-journal-123",
|
|
595
|
+
summary="Updated via Events",
|
|
596
|
+
dtstart=datetime.now(timezone.utc),
|
|
597
|
+
calendar_uid="cal-123",
|
|
598
|
+
account_alias="test",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
result = journal_manager.update_journal(
|
|
602
|
+
journal_uid="test-journal-123",
|
|
603
|
+
calendar_uid="cal-123",
|
|
604
|
+
summary="Updated via Events",
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
assert result is not None
|
|
608
|
+
mock_event.save.assert_called_once()
|
|
609
|
+
|
|
610
|
+
def test_update_journal_not_found(self, journal_manager):
|
|
611
|
+
"""Test journal update when journal not found"""
|
|
612
|
+
mock_calendar = Mock()
|
|
613
|
+
mock_calendar.event_by_uid.side_effect = Exception("Not found")
|
|
614
|
+
mock_calendar.journals.return_value = []
|
|
615
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
616
|
+
|
|
617
|
+
with pytest.raises(JournalNotFoundError):
|
|
618
|
+
journal_manager.update_journal(
|
|
619
|
+
journal_uid="nonexistent", calendar_uid="cal-123", summary="Won't work"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
def test_update_journal_invalid_ical_data(self, journal_manager):
|
|
623
|
+
"""Test journal update with invalid iCalendar data"""
|
|
624
|
+
mock_calendar = Mock()
|
|
625
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
626
|
+
|
|
627
|
+
mock_caldav_journal = Mock()
|
|
628
|
+
mock_caldav_journal.data = "INVALID ICAL DATA"
|
|
629
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_journal
|
|
630
|
+
|
|
631
|
+
with pytest.raises(EventCreationError):
|
|
632
|
+
journal_manager.update_journal(
|
|
633
|
+
journal_uid="test-journal-123",
|
|
634
|
+
calendar_uid="cal-123",
|
|
635
|
+
summary="Updated",
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
def test_delete_journal_success_with_event_by_uid(self, journal_manager):
|
|
639
|
+
"""Test successful journal deletion using event_by_uid"""
|
|
640
|
+
mock_calendar = Mock()
|
|
641
|
+
mock_calendar.event_by_uid = Mock()
|
|
642
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
643
|
+
|
|
644
|
+
mock_journal = Mock()
|
|
645
|
+
mock_journal.delete = Mock()
|
|
646
|
+
mock_calendar.event_by_uid.return_value = mock_journal
|
|
647
|
+
|
|
648
|
+
result = journal_manager.delete_journal(
|
|
649
|
+
calendar_uid="cal-123", journal_uid="test-journal-123"
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
assert result is True
|
|
653
|
+
mock_journal.delete.assert_called_once()
|
|
654
|
+
|
|
655
|
+
def test_delete_journal_fallback_to_journals_search(self, journal_manager):
|
|
656
|
+
"""Test journal deletion fallback to searching through journals()"""
|
|
657
|
+
mock_calendar = Mock()
|
|
658
|
+
mock_calendar.event_by_uid.side_effect = Exception("event_by_uid failed")
|
|
659
|
+
mock_calendar.journals = Mock()
|
|
660
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
661
|
+
|
|
662
|
+
# Create mock journal with VJOURNAL component that contains the target UID
|
|
663
|
+
cal1 = iCalendar()
|
|
664
|
+
event1 = iJournal() # Wrong component type for first mock
|
|
665
|
+
event1.add("uid", "different-uid")
|
|
666
|
+
cal1.add_component(event1)
|
|
667
|
+
|
|
668
|
+
cal2 = iCalendar()
|
|
669
|
+
journal2 = iJournal()
|
|
670
|
+
journal2.add("uid", "test-journal-123") # This matches what we're searching for
|
|
671
|
+
journal2.add("summary", "Target Journal")
|
|
672
|
+
cal2.add_component(journal2)
|
|
673
|
+
|
|
674
|
+
mock_journal1 = Mock()
|
|
675
|
+
mock_journal1.data = cal1.to_ical().decode("utf-8")
|
|
676
|
+
mock_journal2 = Mock()
|
|
677
|
+
mock_journal2.data = cal2.to_ical().decode("utf-8")
|
|
678
|
+
mock_journal2.delete = Mock()
|
|
679
|
+
mock_calendar.journals.return_value = [mock_journal1, mock_journal2]
|
|
680
|
+
|
|
681
|
+
result = journal_manager.delete_journal(
|
|
682
|
+
calendar_uid="cal-123", journal_uid="test-journal-123"
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
assert result is True
|
|
686
|
+
mock_journal2.delete.assert_called_once()
|
|
687
|
+
|
|
688
|
+
def test_delete_journal_fallback_to_events_search(self, journal_manager):
|
|
689
|
+
"""Test journal deletion fallback to searching through events()"""
|
|
690
|
+
# Setup calendar without journals method
|
|
691
|
+
mock_calendar = Mock()
|
|
692
|
+
mock_calendar.event_by_uid.side_effect = Exception("event_by_uid failed")
|
|
693
|
+
mock_calendar.events = Mock()
|
|
694
|
+
if hasattr(mock_calendar, "journals"):
|
|
695
|
+
delattr(mock_calendar, "journals")
|
|
696
|
+
|
|
697
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
698
|
+
|
|
699
|
+
# Create mock event with VJOURNAL component
|
|
700
|
+
target_ical = self._create_journal_ical()
|
|
701
|
+
mock_event = Mock()
|
|
702
|
+
mock_event.data = target_ical
|
|
703
|
+
mock_event.delete = Mock()
|
|
704
|
+
mock_calendar.events.return_value = [mock_event]
|
|
705
|
+
|
|
706
|
+
result = journal_manager.delete_journal(
|
|
707
|
+
calendar_uid="cal-123", journal_uid="test-journal-123"
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
assert result is True
|
|
711
|
+
mock_event.delete.assert_called_once()
|
|
712
|
+
|
|
713
|
+
def test_delete_journal_not_found(self, journal_manager):
|
|
714
|
+
"""Test journal deletion when journal not found"""
|
|
715
|
+
mock_calendar = Mock()
|
|
716
|
+
mock_calendar.event_by_uid.side_effect = Exception("Not found")
|
|
717
|
+
mock_calendar.journals.return_value = []
|
|
718
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
719
|
+
|
|
720
|
+
with pytest.raises(JournalNotFoundError):
|
|
721
|
+
journal_manager.delete_journal(
|
|
722
|
+
calendar_uid="cal-123", journal_uid="nonexistent"
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
def test_delete_journal_authorization_error(self, journal_manager):
|
|
726
|
+
"""Test journal deletion with authorization error"""
|
|
727
|
+
from caldav.lib.error import AuthorizationError
|
|
728
|
+
|
|
729
|
+
mock_calendar = Mock()
|
|
730
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
731
|
+
|
|
732
|
+
mock_journal = Mock()
|
|
733
|
+
mock_journal.delete.side_effect = AuthorizationError("Unauthorized")
|
|
734
|
+
mock_calendar.event_by_uid.return_value = mock_journal
|
|
735
|
+
|
|
736
|
+
# Execute & Verify - when journal is found but deletion fails due to auth, raises EventDeletionError
|
|
737
|
+
# (not JournalNotFoundError, since the journal was successfully found)
|
|
738
|
+
with pytest.raises(EventDeletionError):
|
|
739
|
+
journal_manager.delete_journal(
|
|
740
|
+
calendar_uid="cal-123", journal_uid="test-journal-123"
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
def test_delete_journal_generic_error(self, journal_manager):
|
|
744
|
+
"""Test journal deletion with generic error"""
|
|
745
|
+
mock_calendar = Mock()
|
|
746
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
747
|
+
|
|
748
|
+
mock_journal = Mock()
|
|
749
|
+
mock_journal.delete.side_effect = Exception("Generic error")
|
|
750
|
+
mock_calendar.event_by_uid.return_value = mock_journal
|
|
751
|
+
|
|
752
|
+
# Execute & Verify - when journal is found but deletion fails, raises EventDeletionError
|
|
753
|
+
# (not JournalNotFoundError, since the journal was successfully found)
|
|
754
|
+
with pytest.raises(EventDeletionError):
|
|
755
|
+
journal_manager.delete_journal(
|
|
756
|
+
calendar_uid="cal-123", journal_uid="test-journal-123"
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
def _create_journal_ical(self):
|
|
760
|
+
"""Create sample iCalendar data with VJOURNAL component"""
|
|
761
|
+
cal = iCalendar()
|
|
762
|
+
journal = iJournal()
|
|
763
|
+
journal.add("uid", "test-journal-123")
|
|
764
|
+
journal.add("summary", "Test Journal")
|
|
765
|
+
journal.add("description", "Test Description")
|
|
766
|
+
journal.add("dtstart", datetime(2025, 7, 10, 9, 0, tzinfo=timezone.utc))
|
|
767
|
+
cal.add_component(journal)
|
|
768
|
+
return cal.to_ical().decode("utf-8")
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
class TestJournalEdgeCases:
|
|
772
|
+
"""Test edge cases, error conditions, and parsing"""
|
|
773
|
+
|
|
774
|
+
@pytest.fixture
|
|
775
|
+
def journal_manager(self):
|
|
776
|
+
"""JournalManager instance for testing"""
|
|
777
|
+
mock_calendar_manager = Mock(spec=CalendarManager)
|
|
778
|
+
return JournalManager(mock_calendar_manager)
|
|
779
|
+
|
|
780
|
+
def test_parse_caldav_journal_success(self, journal_manager):
|
|
781
|
+
"""Test successful VJOURNAL parsing"""
|
|
782
|
+
mock_caldav_event = Mock()
|
|
783
|
+
mock_caldav_event.data = self._create_complex_journal_ical()
|
|
784
|
+
|
|
785
|
+
result = journal_manager._parse_caldav_journal(
|
|
786
|
+
mock_caldav_event, "cal-123", "test_account"
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
assert result is not None
|
|
790
|
+
assert result.uid == "complex-journal-123"
|
|
791
|
+
assert result.summary == "Complex Journal"
|
|
792
|
+
assert result.description == "Detailed description"
|
|
793
|
+
# Categories will be converted to strings from icalendar objects
|
|
794
|
+
assert len(result.categories) == 1 # The list becomes a single string
|
|
795
|
+
assert len(result.related_to) == 2
|
|
796
|
+
assert "event-456" in result.related_to
|
|
797
|
+
assert "task-789" in result.related_to
|
|
798
|
+
|
|
799
|
+
def test_parse_caldav_journal_with_single_category(self, journal_manager):
|
|
800
|
+
"""Test VJOURNAL parsing with single category (not list)"""
|
|
801
|
+
cal = iCalendar()
|
|
802
|
+
journal = iJournal()
|
|
803
|
+
journal.add("uid", "single-cat-journal")
|
|
804
|
+
journal.add("summary", "Single Category Journal")
|
|
805
|
+
journal.add("categories", "personal") # Single category, not list
|
|
806
|
+
cal.add_component(journal)
|
|
807
|
+
|
|
808
|
+
mock_caldav_event = Mock()
|
|
809
|
+
mock_caldav_event.data = cal.to_ical().decode("utf-8")
|
|
810
|
+
|
|
811
|
+
result = journal_manager._parse_caldav_journal(
|
|
812
|
+
mock_caldav_event, "cal-123", "test_account"
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
assert result is not None
|
|
816
|
+
# The icalendar library wraps categories in objects that need string conversion
|
|
817
|
+
assert len(result.categories) == 1
|
|
818
|
+
# Categories are converted to string representations that contain the original value
|
|
819
|
+
category_str = str(result.categories[0])
|
|
820
|
+
assert "vCategory" in category_str # Just verify it's the expected object type
|
|
821
|
+
|
|
822
|
+
def test_parse_caldav_journal_with_single_related_to(self, journal_manager):
|
|
823
|
+
"""Test VJOURNAL parsing with single related-to (not list)"""
|
|
824
|
+
cal = iCalendar()
|
|
825
|
+
journal = iJournal()
|
|
826
|
+
journal.add("uid", "single-related-journal")
|
|
827
|
+
journal.add("summary", "Single Related Journal")
|
|
828
|
+
journal.add("related-to", "event-123") # Single related-to, not list
|
|
829
|
+
cal.add_component(journal)
|
|
830
|
+
|
|
831
|
+
mock_caldav_event = Mock()
|
|
832
|
+
mock_caldav_event.data = cal.to_ical().decode("utf-8")
|
|
833
|
+
|
|
834
|
+
result = journal_manager._parse_caldav_journal(
|
|
835
|
+
mock_caldav_event, "cal-123", "test_account"
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
assert result is not None
|
|
839
|
+
assert result.related_to == ["event-123"]
|
|
840
|
+
|
|
841
|
+
def test_parse_caldav_journal_minimal_data(self, journal_manager):
|
|
842
|
+
"""Test VJOURNAL parsing with minimal required data"""
|
|
843
|
+
cal = iCalendar()
|
|
844
|
+
journal = iJournal()
|
|
845
|
+
journal.add("uid", "minimal-journal")
|
|
846
|
+
# Only UID, no summary or other fields
|
|
847
|
+
cal.add_component(journal)
|
|
848
|
+
|
|
849
|
+
mock_caldav_event = Mock()
|
|
850
|
+
mock_caldav_event.data = cal.to_ical().decode("utf-8")
|
|
851
|
+
|
|
852
|
+
with patch("chronos_mcp.journals.datetime") as mock_datetime:
|
|
853
|
+
mock_now = datetime(2025, 7, 10, 12, 0, tzinfo=timezone.utc)
|
|
854
|
+
mock_datetime.now.return_value = mock_now
|
|
855
|
+
mock_datetime.timezone = timezone
|
|
856
|
+
|
|
857
|
+
result = journal_manager._parse_caldav_journal(
|
|
858
|
+
mock_caldav_event, "cal-123", "test_account"
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
assert result is not None
|
|
862
|
+
assert result.uid == "minimal-journal"
|
|
863
|
+
assert result.summary == "No Title" # Default value
|
|
864
|
+
assert result.description is None
|
|
865
|
+
assert result.dtstart == mock_now # Uses current time as fallback
|
|
866
|
+
|
|
867
|
+
def test_parse_caldav_journal_no_vjournal_component(self, journal_manager):
|
|
868
|
+
"""Test parsing CalDAV data without VJOURNAL component"""
|
|
869
|
+
# Create calendar with only VEVENT
|
|
870
|
+
cal = iCalendar()
|
|
871
|
+
from icalendar import Event as iEvent
|
|
872
|
+
|
|
873
|
+
event = iEvent()
|
|
874
|
+
event.add("uid", "event-123")
|
|
875
|
+
event.add("summary", "Regular Event")
|
|
876
|
+
cal.add_component(event)
|
|
877
|
+
|
|
878
|
+
mock_caldav_event = Mock()
|
|
879
|
+
mock_caldav_event.data = cal.to_ical().decode("utf-8")
|
|
880
|
+
|
|
881
|
+
result = journal_manager._parse_caldav_journal(
|
|
882
|
+
mock_caldav_event, "cal-123", "test_account"
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
assert result is None
|
|
886
|
+
|
|
887
|
+
def test_parse_caldav_journal_invalid_ical_data(self, journal_manager):
|
|
888
|
+
"""Test parsing invalid iCalendar data"""
|
|
889
|
+
mock_caldav_event = Mock()
|
|
890
|
+
mock_caldav_event.data = "INVALID ICAL DATA"
|
|
891
|
+
|
|
892
|
+
result = journal_manager._parse_caldav_journal(
|
|
893
|
+
mock_caldav_event, "cal-123", "test_account"
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
assert result is None
|
|
897
|
+
|
|
898
|
+
def test_parse_caldav_journal_exception_during_parsing(self, journal_manager):
|
|
899
|
+
"""Test handling exceptions during journal parsing"""
|
|
900
|
+
mock_caldav_event = Mock()
|
|
901
|
+
mock_caldav_event.data = self._create_complex_journal_ical()
|
|
902
|
+
|
|
903
|
+
with patch(
|
|
904
|
+
"icalendar.Calendar.from_ical", side_effect=Exception("Parse error")
|
|
905
|
+
):
|
|
906
|
+
result = journal_manager._parse_caldav_journal(
|
|
907
|
+
mock_caldav_event, "cal-123", "test_account"
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
assert result is None
|
|
911
|
+
|
|
912
|
+
def test_parse_caldav_journal_with_default_account_fallback(self, journal_manager):
|
|
913
|
+
"""Test journal parsing with default account fallback"""
|
|
914
|
+
journal_manager._get_default_account = Mock(return_value="default_account")
|
|
915
|
+
|
|
916
|
+
mock_caldav_event = Mock()
|
|
917
|
+
mock_caldav_event.data = self._create_simple_journal_ical()
|
|
918
|
+
|
|
919
|
+
result = journal_manager._parse_caldav_journal(
|
|
920
|
+
mock_caldav_event, "cal-123", None # No account_alias provided
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
assert result is not None
|
|
924
|
+
assert result.account_alias == "default_account"
|
|
925
|
+
|
|
926
|
+
def test_parse_caldav_journal_no_default_account(self, journal_manager):
|
|
927
|
+
"""Test journal parsing when no default account available"""
|
|
928
|
+
journal_manager._get_default_account = Mock(return_value=None)
|
|
929
|
+
|
|
930
|
+
mock_caldav_event = Mock()
|
|
931
|
+
mock_caldav_event.data = self._create_simple_journal_ical()
|
|
932
|
+
|
|
933
|
+
result = journal_manager._parse_caldav_journal(
|
|
934
|
+
mock_caldav_event, "cal-123", None # No account_alias provided
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
assert result is not None
|
|
938
|
+
assert result.account_alias == "default" # Final fallback
|
|
939
|
+
|
|
940
|
+
def test_update_journal_update_all_fields(self, journal_manager):
|
|
941
|
+
"""Test updating all journal fields"""
|
|
942
|
+
mock_calendar = Mock()
|
|
943
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
944
|
+
|
|
945
|
+
existing_ical = self._create_complex_journal_ical()
|
|
946
|
+
mock_caldav_journal = Mock()
|
|
947
|
+
mock_caldav_journal.data = existing_ical
|
|
948
|
+
mock_caldav_journal.save = Mock()
|
|
949
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_journal
|
|
950
|
+
|
|
951
|
+
new_dtstart = datetime(2025, 8, 1, 14, 0, tzinfo=timezone.utc)
|
|
952
|
+
new_related_to = ["new-event-123", "new-task-456"]
|
|
953
|
+
|
|
954
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
955
|
+
mock_parse.return_value = Journal(
|
|
956
|
+
uid="complex-journal-123",
|
|
957
|
+
summary="Updated Summary",
|
|
958
|
+
description="Updated Description",
|
|
959
|
+
dtstart=new_dtstart,
|
|
960
|
+
related_to=new_related_to,
|
|
961
|
+
calendar_uid="cal-123",
|
|
962
|
+
account_alias="test",
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
result = journal_manager.update_journal(
|
|
966
|
+
journal_uid="complex-journal-123",
|
|
967
|
+
calendar_uid="cal-123",
|
|
968
|
+
summary="Updated Summary",
|
|
969
|
+
description="Updated Description",
|
|
970
|
+
dtstart=new_dtstart,
|
|
971
|
+
related_to=new_related_to,
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
assert result is not None
|
|
975
|
+
mock_caldav_journal.save.assert_called_once()
|
|
976
|
+
|
|
977
|
+
def test_update_journal_clear_description(self, journal_manager):
|
|
978
|
+
"""Test updating journal to clear description field"""
|
|
979
|
+
mock_calendar = Mock()
|
|
980
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
981
|
+
|
|
982
|
+
existing_ical = self._create_complex_journal_ical()
|
|
983
|
+
mock_caldav_journal = Mock()
|
|
984
|
+
mock_caldav_journal.data = existing_ical
|
|
985
|
+
mock_caldav_journal.save = Mock()
|
|
986
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_journal
|
|
987
|
+
|
|
988
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
989
|
+
mock_parse.return_value = Journal(
|
|
990
|
+
uid="complex-journal-123",
|
|
991
|
+
summary="Complex Journal",
|
|
992
|
+
description=None, # Cleared description
|
|
993
|
+
dtstart=datetime.now(timezone.utc),
|
|
994
|
+
calendar_uid="cal-123",
|
|
995
|
+
account_alias="test",
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
result = journal_manager.update_journal(
|
|
999
|
+
journal_uid="complex-journal-123",
|
|
1000
|
+
calendar_uid="cal-123",
|
|
1001
|
+
description="", # Empty string to clear
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
assert result is not None
|
|
1005
|
+
assert result.description is None
|
|
1006
|
+
|
|
1007
|
+
def test_update_journal_clear_related_to(self, journal_manager):
|
|
1008
|
+
"""Test updating journal to clear related_to field"""
|
|
1009
|
+
mock_calendar = Mock()
|
|
1010
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
1011
|
+
|
|
1012
|
+
existing_ical = self._create_complex_journal_ical()
|
|
1013
|
+
mock_caldav_journal = Mock()
|
|
1014
|
+
mock_caldav_journal.data = existing_ical
|
|
1015
|
+
mock_caldav_journal.save = Mock()
|
|
1016
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_journal
|
|
1017
|
+
|
|
1018
|
+
with patch.object(journal_manager, "_parse_caldav_journal") as mock_parse:
|
|
1019
|
+
mock_parse.return_value = Journal(
|
|
1020
|
+
uid="complex-journal-123",
|
|
1021
|
+
summary="Complex Journal",
|
|
1022
|
+
dtstart=datetime.now(timezone.utc),
|
|
1023
|
+
related_to=[], # Cleared related_to
|
|
1024
|
+
calendar_uid="cal-123",
|
|
1025
|
+
account_alias="test",
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
result = journal_manager.update_journal(
|
|
1029
|
+
journal_uid="complex-journal-123",
|
|
1030
|
+
calendar_uid="cal-123",
|
|
1031
|
+
related_to=[], # Empty list to clear
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
assert result is not None
|
|
1035
|
+
assert result.related_to == []
|
|
1036
|
+
|
|
1037
|
+
def test_create_journal_with_request_id(self, journal_manager):
|
|
1038
|
+
"""Test journal creation with custom request_id"""
|
|
1039
|
+
mock_calendar = Mock()
|
|
1040
|
+
mock_calendar.save_journal = Mock()
|
|
1041
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
1042
|
+
|
|
1043
|
+
custom_request_id = "custom-request-123"
|
|
1044
|
+
|
|
1045
|
+
with patch("uuid.uuid4", return_value="test-uid-123"):
|
|
1046
|
+
result = journal_manager.create_journal(
|
|
1047
|
+
calendar_uid="cal-123",
|
|
1048
|
+
summary="Test Journal",
|
|
1049
|
+
request_id=custom_request_id,
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
assert result is not None
|
|
1053
|
+
# Verify request_id was passed through to get_calendar
|
|
1054
|
+
journal_manager.calendars.get_calendar.assert_called_with(
|
|
1055
|
+
"cal-123", None, request_id=custom_request_id
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
def test_operations_generate_request_id_when_none_provided(self, journal_manager):
|
|
1059
|
+
"""Test that operations generate request_id when none provided"""
|
|
1060
|
+
mock_calendar = Mock()
|
|
1061
|
+
mock_calendar.save_journal = Mock()
|
|
1062
|
+
journal_manager.calendars.get_calendar.return_value = mock_calendar
|
|
1063
|
+
|
|
1064
|
+
with patch("uuid.uuid4", return_value="generated-request-id") as mock_uuid:
|
|
1065
|
+
result = journal_manager.create_journal(
|
|
1066
|
+
calendar_uid="cal-123",
|
|
1067
|
+
summary="Test Journal",
|
|
1068
|
+
# No request_id provided
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
assert result is not None
|
|
1072
|
+
# Should be called twice: once for request_id, once for journal UID
|
|
1073
|
+
assert mock_uuid.call_count == 2
|
|
1074
|
+
|
|
1075
|
+
def _create_simple_journal_ical(self):
|
|
1076
|
+
"""Create simple iCalendar data with VJOURNAL"""
|
|
1077
|
+
cal = iCalendar()
|
|
1078
|
+
journal = iJournal()
|
|
1079
|
+
journal.add("uid", "simple-journal-123")
|
|
1080
|
+
journal.add("summary", "Simple Journal")
|
|
1081
|
+
journal.add("dtstart", datetime(2025, 7, 10, 9, 0, tzinfo=timezone.utc))
|
|
1082
|
+
cal.add_component(journal)
|
|
1083
|
+
return cal.to_ical().decode("utf-8")
|
|
1084
|
+
|
|
1085
|
+
def _create_complex_journal_ical(self):
|
|
1086
|
+
"""Create complex iCalendar data with VJOURNAL including categories and related-to"""
|
|
1087
|
+
cal = iCalendar()
|
|
1088
|
+
journal = iJournal()
|
|
1089
|
+
journal.add("uid", "complex-journal-123")
|
|
1090
|
+
journal.add("summary", "Complex Journal")
|
|
1091
|
+
journal.add("description", "Detailed description")
|
|
1092
|
+
journal.add("dtstart", datetime(2025, 7, 10, 9, 0, tzinfo=timezone.utc))
|
|
1093
|
+
journal.add("categories", ["work", "project"])
|
|
1094
|
+
journal.add("related-to", "event-456")
|
|
1095
|
+
journal.add("related-to", "task-789")
|
|
1096
|
+
cal.add_component(journal)
|
|
1097
|
+
return cal.to_ical().decode("utf-8")
|