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.
Files changed (68) hide show
  1. chronos_mcp/__init__.py +5 -0
  2. chronos_mcp/__main__.py +9 -0
  3. chronos_mcp/accounts.py +410 -0
  4. chronos_mcp/bulk.py +946 -0
  5. chronos_mcp/caldav_utils.py +149 -0
  6. chronos_mcp/calendars.py +204 -0
  7. chronos_mcp/config.py +187 -0
  8. chronos_mcp/credentials.py +190 -0
  9. chronos_mcp/events.py +515 -0
  10. chronos_mcp/exceptions.py +477 -0
  11. chronos_mcp/journals.py +477 -0
  12. chronos_mcp/logging_config.py +23 -0
  13. chronos_mcp/models.py +202 -0
  14. chronos_mcp/py.typed +0 -0
  15. chronos_mcp/rrule.py +259 -0
  16. chronos_mcp/search.py +315 -0
  17. chronos_mcp/server.py +121 -0
  18. chronos_mcp/tasks.py +518 -0
  19. chronos_mcp/tools/__init__.py +29 -0
  20. chronos_mcp/tools/accounts.py +151 -0
  21. chronos_mcp/tools/base.py +59 -0
  22. chronos_mcp/tools/bulk.py +557 -0
  23. chronos_mcp/tools/calendars.py +142 -0
  24. chronos_mcp/tools/events.py +698 -0
  25. chronos_mcp/tools/journals.py +310 -0
  26. chronos_mcp/tools/tasks.py +414 -0
  27. chronos_mcp/utils.py +163 -0
  28. chronos_mcp/validation.py +636 -0
  29. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
  30. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
  31. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
  32. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
  33. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
  34. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
  35. tests/__init__.py +0 -0
  36. tests/conftest.py +91 -0
  37. tests/unit/__init__.py +0 -0
  38. tests/unit/test_accounts.py +380 -0
  39. tests/unit/test_accounts_ssrf.py +134 -0
  40. tests/unit/test_base.py +135 -0
  41. tests/unit/test_bulk.py +380 -0
  42. tests/unit/test_bulk_create.py +408 -0
  43. tests/unit/test_bulk_delete.py +341 -0
  44. tests/unit/test_bulk_resource_limits.py +74 -0
  45. tests/unit/test_caldav_utils.py +300 -0
  46. tests/unit/test_calendars.py +286 -0
  47. tests/unit/test_config.py +111 -0
  48. tests/unit/test_config_validation.py +128 -0
  49. tests/unit/test_credentials_security.py +189 -0
  50. tests/unit/test_cryptography_security.py +178 -0
  51. tests/unit/test_events.py +536 -0
  52. tests/unit/test_exceptions.py +58 -0
  53. tests/unit/test_journals.py +1097 -0
  54. tests/unit/test_models.py +95 -0
  55. tests/unit/test_race_conditions.py +202 -0
  56. tests/unit/test_recurring_events.py +156 -0
  57. tests/unit/test_rrule.py +217 -0
  58. tests/unit/test_search.py +372 -0
  59. tests/unit/test_search_advanced.py +333 -0
  60. tests/unit/test_server_input_validation.py +219 -0
  61. tests/unit/test_ssrf_protection.py +505 -0
  62. tests/unit/test_tasks.py +918 -0
  63. tests/unit/test_thread_safety.py +301 -0
  64. tests/unit/test_tools_journals.py +617 -0
  65. tests/unit/test_tools_tasks.py +968 -0
  66. tests/unit/test_url_validation_security.py +234 -0
  67. tests/unit/test_utils.py +180 -0
  68. 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")