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,617 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive unit tests for chronos_mcp/tools/journals.py module
|
|
3
|
+
Tests all MCP journal tool functions for 100% coverage with defensive programming patterns
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from unittest.mock import MagicMock, Mock, patch, AsyncMock
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from chronos_mcp.tools.journals import (
|
|
13
|
+
create_journal,
|
|
14
|
+
list_journals,
|
|
15
|
+
update_journal,
|
|
16
|
+
delete_journal,
|
|
17
|
+
register_journal_tools,
|
|
18
|
+
_managers,
|
|
19
|
+
)
|
|
20
|
+
from chronos_mcp.exceptions import (
|
|
21
|
+
CalendarNotFoundError,
|
|
22
|
+
EventNotFoundError,
|
|
23
|
+
EventCreationError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
ChronosError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestJournalTools:
|
|
30
|
+
"""Test MCP journal tool functions with comprehensive coverage"""
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def mock_managers(self):
|
|
34
|
+
"""Mock managers for dependency injection"""
|
|
35
|
+
journal_manager = Mock()
|
|
36
|
+
return {"journal_manager": journal_manager}
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def sample_journal(self):
|
|
40
|
+
"""Sample journal object for testing"""
|
|
41
|
+
journal = Mock()
|
|
42
|
+
journal.uid = "journal-123"
|
|
43
|
+
journal.summary = "Test Journal"
|
|
44
|
+
journal.description = "Test journal content"
|
|
45
|
+
journal.dtstart = datetime(2025, 12, 31, 23, 59, tzinfo=timezone.utc)
|
|
46
|
+
journal.related_to = ["related-1", "related-2"]
|
|
47
|
+
return journal
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def setup_managers(self, mock_managers):
|
|
51
|
+
"""Setup _managers module variable"""
|
|
52
|
+
original = _managers.copy()
|
|
53
|
+
_managers.clear()
|
|
54
|
+
_managers.update(mock_managers)
|
|
55
|
+
yield
|
|
56
|
+
_managers.clear()
|
|
57
|
+
_managers.update(original)
|
|
58
|
+
|
|
59
|
+
# CREATE_JOURNAL TOOL TESTS
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_create_journal_minimal_success(self, setup_managers, sample_journal):
|
|
63
|
+
"""Test create_journal with minimal required parameters"""
|
|
64
|
+
_managers["journal_manager"].create_journal.return_value = sample_journal
|
|
65
|
+
|
|
66
|
+
result = await create_journal.fn(
|
|
67
|
+
calendar_uid="cal-123",
|
|
68
|
+
summary="Test Journal",
|
|
69
|
+
description=None,
|
|
70
|
+
entry_date=None,
|
|
71
|
+
related_to=None,
|
|
72
|
+
account=None,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert result["success"] is True
|
|
76
|
+
assert result["journal"]["uid"] == "journal-123"
|
|
77
|
+
assert result["journal"]["summary"] == "Test Journal"
|
|
78
|
+
assert "request_id" in result
|
|
79
|
+
_managers["journal_manager"].create_journal.assert_called_once()
|
|
80
|
+
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def test_create_journal_full_parameters(self, setup_managers, sample_journal):
|
|
83
|
+
"""Test create_journal with all parameters provided"""
|
|
84
|
+
_managers["journal_manager"].create_journal.return_value = sample_journal
|
|
85
|
+
|
|
86
|
+
result = await create_journal.fn(
|
|
87
|
+
calendar_uid="cal-123",
|
|
88
|
+
summary="Full Test Journal",
|
|
89
|
+
description="Full journal content",
|
|
90
|
+
entry_date="2025-12-31T23:59:00Z",
|
|
91
|
+
related_to=["related-1", "related-2"],
|
|
92
|
+
account="test_account",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert result["success"] is True
|
|
96
|
+
assert result["journal"]["summary"] == "Test Journal" # from sample_journal
|
|
97
|
+
_managers["journal_manager"].create_journal.assert_called_once()
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_create_journal_summary_validation_error(self, setup_managers):
|
|
101
|
+
"""Test create_journal validation error for summary"""
|
|
102
|
+
with patch(
|
|
103
|
+
"chronos_mcp.tools.journals.InputValidator.validate_text_field"
|
|
104
|
+
) as mock_validate:
|
|
105
|
+
mock_validate.side_effect = ValidationError("Summary too long")
|
|
106
|
+
|
|
107
|
+
result = await create_journal.fn(
|
|
108
|
+
calendar_uid="cal-123",
|
|
109
|
+
summary="x" * 1000, # Very long summary
|
|
110
|
+
description=None,
|
|
111
|
+
entry_date=None,
|
|
112
|
+
related_to=None,
|
|
113
|
+
account=None,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert result["success"] is False
|
|
117
|
+
assert "Summary too long" in result["error"]
|
|
118
|
+
assert result["error_code"] == "VALIDATION_ERROR"
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_create_journal_description_validation_error(self, setup_managers):
|
|
122
|
+
"""Test create_journal validation error for description"""
|
|
123
|
+
with patch(
|
|
124
|
+
"chronos_mcp.tools.journals.InputValidator.validate_text_field"
|
|
125
|
+
) as mock_validate:
|
|
126
|
+
# Summary passes, description fails
|
|
127
|
+
mock_validate.side_effect = [
|
|
128
|
+
"Valid Summary", # First call for summary
|
|
129
|
+
ValidationError("Description invalid"), # Second call for description
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
result = await create_journal.fn(
|
|
133
|
+
calendar_uid="cal-123",
|
|
134
|
+
summary="Valid Summary",
|
|
135
|
+
description="Invalid description",
|
|
136
|
+
entry_date=None,
|
|
137
|
+
related_to=None,
|
|
138
|
+
account=None,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
assert result["success"] is False
|
|
142
|
+
assert "Description invalid" in result["error"]
|
|
143
|
+
assert result["error_code"] == "VALIDATION_ERROR"
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_create_journal_entry_date_none(self, setup_managers, sample_journal):
|
|
147
|
+
"""Test create_journal with entry date as None in response"""
|
|
148
|
+
sample_journal.dtstart = None
|
|
149
|
+
_managers["journal_manager"].create_journal.return_value = sample_journal
|
|
150
|
+
|
|
151
|
+
result = await create_journal.fn(
|
|
152
|
+
calendar_uid="cal-123",
|
|
153
|
+
summary="Test Journal",
|
|
154
|
+
description=None,
|
|
155
|
+
entry_date=None,
|
|
156
|
+
related_to=None,
|
|
157
|
+
account=None,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert result["success"] is True
|
|
161
|
+
assert result["journal"]["entry_date"] is None
|
|
162
|
+
|
|
163
|
+
@pytest.mark.asyncio
|
|
164
|
+
async def test_create_journal_chronos_error(self, setup_managers):
|
|
165
|
+
"""Test create_journal handles ChronosError"""
|
|
166
|
+
error = ChronosError("General error")
|
|
167
|
+
_managers["journal_manager"].create_journal.side_effect = error
|
|
168
|
+
|
|
169
|
+
result = await create_journal.fn(
|
|
170
|
+
calendar_uid="cal-123",
|
|
171
|
+
summary="Test Journal",
|
|
172
|
+
description=None,
|
|
173
|
+
entry_date=None,
|
|
174
|
+
related_to=None,
|
|
175
|
+
account=None,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
assert result["success"] is False
|
|
179
|
+
assert result["error_code"] == "ChronosError"
|
|
180
|
+
|
|
181
|
+
@pytest.mark.asyncio
|
|
182
|
+
async def test_create_journal_unexpected_exception(self, setup_managers):
|
|
183
|
+
"""Test create_journal handles unexpected exceptions"""
|
|
184
|
+
_managers["journal_manager"].create_journal.side_effect = RuntimeError(
|
|
185
|
+
"Unexpected error"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
result = await create_journal.fn(
|
|
189
|
+
calendar_uid="cal-123",
|
|
190
|
+
summary="Test Journal",
|
|
191
|
+
description=None,
|
|
192
|
+
entry_date=None,
|
|
193
|
+
related_to=None,
|
|
194
|
+
account=None,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert result["success"] is False
|
|
198
|
+
assert "Failed to create journal" in result["error"]
|
|
199
|
+
assert "request_id" in result
|
|
200
|
+
|
|
201
|
+
@pytest.mark.asyncio
|
|
202
|
+
async def test_create_journal_malformed_entry_date(self, setup_managers):
|
|
203
|
+
"""Test create_journal with malformed entry date triggering parse_datetime error"""
|
|
204
|
+
with patch("chronos_mcp.tools.journals.parse_datetime") as mock_parse:
|
|
205
|
+
mock_parse.side_effect = ValueError("Invalid date format")
|
|
206
|
+
|
|
207
|
+
result = await create_journal.fn(
|
|
208
|
+
calendar_uid="cal-123",
|
|
209
|
+
summary="Test Journal",
|
|
210
|
+
description=None,
|
|
211
|
+
entry_date="invalid-date",
|
|
212
|
+
related_to=None,
|
|
213
|
+
account=None,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
assert result["success"] is False
|
|
217
|
+
assert "Failed to create journal" in result["error"]
|
|
218
|
+
|
|
219
|
+
# LIST_JOURNALS TOOL TESTS
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
async def test_list_journals_success(self, setup_managers, sample_journal):
|
|
223
|
+
"""Test list_journals successful execution"""
|
|
224
|
+
_managers["journal_manager"].list_journals.return_value = [sample_journal]
|
|
225
|
+
|
|
226
|
+
result = await list_journals.fn(calendar_uid="cal-123", account=None, limit=50)
|
|
227
|
+
|
|
228
|
+
assert len(result["journals"]) == 1
|
|
229
|
+
assert result["total"] == 1
|
|
230
|
+
assert result["calendar_uid"] == "cal-123"
|
|
231
|
+
assert "request_id" in result
|
|
232
|
+
|
|
233
|
+
@pytest.mark.asyncio
|
|
234
|
+
async def test_list_journals_with_account_and_limit(
|
|
235
|
+
self, setup_managers, sample_journal
|
|
236
|
+
):
|
|
237
|
+
"""Test list_journals with account and limit parameters"""
|
|
238
|
+
_managers["journal_manager"].list_journals.return_value = [sample_journal]
|
|
239
|
+
|
|
240
|
+
result = await list_journals.fn(
|
|
241
|
+
calendar_uid="cal-123", account="test_account", limit=10
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
assert len(result["journals"]) == 1
|
|
245
|
+
_managers["journal_manager"].list_journals.assert_called_once_with(
|
|
246
|
+
calendar_uid="cal-123", limit=10, account_alias="test_account"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
@pytest.mark.asyncio
|
|
250
|
+
async def test_list_journals_limit_string_conversion(
|
|
251
|
+
self, setup_managers, sample_journal
|
|
252
|
+
):
|
|
253
|
+
"""Test list_journals converts string limit to int"""
|
|
254
|
+
_managers["journal_manager"].list_journals.return_value = [sample_journal]
|
|
255
|
+
|
|
256
|
+
result = await list_journals.fn(
|
|
257
|
+
calendar_uid="cal-123",
|
|
258
|
+
account=None,
|
|
259
|
+
limit="25", # String that should convert to int
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
assert len(result["journals"]) == 1
|
|
263
|
+
_managers["journal_manager"].list_journals.assert_called_once_with(
|
|
264
|
+
calendar_uid="cal-123", limit=25, account_alias=None
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
@pytest.mark.asyncio
|
|
268
|
+
async def test_list_journals_invalid_limit_string(self, setup_managers):
|
|
269
|
+
"""Test list_journals handles invalid limit string"""
|
|
270
|
+
result = await list_journals.fn(
|
|
271
|
+
calendar_uid="cal-123",
|
|
272
|
+
account=None,
|
|
273
|
+
limit="invalid", # Cannot convert to int
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
assert result["journals"] == []
|
|
277
|
+
assert result["total"] == 0
|
|
278
|
+
assert "Invalid limit value" in result["error"]
|
|
279
|
+
assert result["error_code"] == "VALIDATION_ERROR"
|
|
280
|
+
assert "request_id" in result
|
|
281
|
+
|
|
282
|
+
@pytest.mark.asyncio
|
|
283
|
+
async def test_list_journals_limit_type_error(self, setup_managers):
|
|
284
|
+
"""Test list_journals handles TypeError in limit conversion"""
|
|
285
|
+
result = await list_journals.fn(
|
|
286
|
+
calendar_uid="cal-123", account=None, limit={} # TypeError when int({})
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
assert result["journals"] == []
|
|
290
|
+
assert result["total"] == 0
|
|
291
|
+
assert "Invalid limit value" in result["error"]
|
|
292
|
+
|
|
293
|
+
@pytest.mark.asyncio
|
|
294
|
+
async def test_list_journals_entry_date_none(self, setup_managers):
|
|
295
|
+
"""Test list_journals with journal having None entry date"""
|
|
296
|
+
journal = Mock()
|
|
297
|
+
journal.uid = "journal-123"
|
|
298
|
+
journal.summary = "Test Journal"
|
|
299
|
+
journal.description = "Test content"
|
|
300
|
+
journal.dtstart = None # No entry date
|
|
301
|
+
journal.related_to = []
|
|
302
|
+
|
|
303
|
+
_managers["journal_manager"].list_journals.return_value = [journal]
|
|
304
|
+
|
|
305
|
+
result = await list_journals.fn(calendar_uid="cal-123", account=None, limit=50)
|
|
306
|
+
|
|
307
|
+
assert result["journals"][0]["entry_date"] is None
|
|
308
|
+
|
|
309
|
+
@pytest.mark.asyncio
|
|
310
|
+
async def test_list_journals_calendar_not_found_error(self, setup_managers):
|
|
311
|
+
"""Test list_journals handles CalendarNotFoundError"""
|
|
312
|
+
error = CalendarNotFoundError("Calendar not found")
|
|
313
|
+
_managers["journal_manager"].list_journals.side_effect = error
|
|
314
|
+
|
|
315
|
+
result = await list_journals.fn(calendar_uid="cal-123", account=None, limit=50)
|
|
316
|
+
|
|
317
|
+
assert result["journals"] == []
|
|
318
|
+
assert result["total"] == 0
|
|
319
|
+
assert "error" in result
|
|
320
|
+
assert "request_id" in result
|
|
321
|
+
|
|
322
|
+
@pytest.mark.asyncio
|
|
323
|
+
async def test_list_journals_chronos_error(self, setup_managers):
|
|
324
|
+
"""Test list_journals handles ChronosError"""
|
|
325
|
+
error = ChronosError("General error")
|
|
326
|
+
_managers["journal_manager"].list_journals.side_effect = error
|
|
327
|
+
|
|
328
|
+
result = await list_journals.fn(calendar_uid="cal-123", account=None, limit=50)
|
|
329
|
+
|
|
330
|
+
assert result["journals"] == []
|
|
331
|
+
assert result["total"] == 0
|
|
332
|
+
assert result["error_code"] == "ChronosError"
|
|
333
|
+
|
|
334
|
+
@pytest.mark.asyncio
|
|
335
|
+
async def test_list_journals_unexpected_exception(self, setup_managers):
|
|
336
|
+
"""Test list_journals handles unexpected exceptions"""
|
|
337
|
+
_managers["journal_manager"].list_journals.side_effect = RuntimeError(
|
|
338
|
+
"Unexpected error"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
result = await list_journals.fn(calendar_uid="cal-123", account=None, limit=50)
|
|
342
|
+
|
|
343
|
+
assert result["journals"] == []
|
|
344
|
+
assert result["total"] == 0
|
|
345
|
+
assert "Failed to list journals" in result["error"]
|
|
346
|
+
|
|
347
|
+
# UPDATE_JOURNAL TOOL TESTS (uses @handle_tool_errors decorator)
|
|
348
|
+
|
|
349
|
+
@pytest.mark.asyncio
|
|
350
|
+
async def test_update_journal_success(self, setup_managers, sample_journal):
|
|
351
|
+
"""Test update_journal successful execution"""
|
|
352
|
+
_managers["journal_manager"].update_journal.return_value = sample_journal
|
|
353
|
+
|
|
354
|
+
result = await update_journal.fn(
|
|
355
|
+
calendar_uid="cal-123",
|
|
356
|
+
journal_uid="journal-123",
|
|
357
|
+
summary="Updated Summary",
|
|
358
|
+
description=None,
|
|
359
|
+
entry_date=None,
|
|
360
|
+
account=None,
|
|
361
|
+
request_id=None,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
assert result["success"] is True
|
|
365
|
+
assert result["journal"]["uid"] == "journal-123"
|
|
366
|
+
assert "request_id" in result
|
|
367
|
+
|
|
368
|
+
@pytest.mark.asyncio
|
|
369
|
+
async def test_update_journal_all_parameters(self, setup_managers, sample_journal):
|
|
370
|
+
"""Test update_journal with all parameters"""
|
|
371
|
+
_managers["journal_manager"].update_journal.return_value = sample_journal
|
|
372
|
+
|
|
373
|
+
result = await update_journal.fn(
|
|
374
|
+
calendar_uid="cal-123",
|
|
375
|
+
journal_uid="journal-123",
|
|
376
|
+
summary="Updated Summary",
|
|
377
|
+
description="Updated content",
|
|
378
|
+
entry_date="2025-12-31T23:59:00Z",
|
|
379
|
+
account="test_account",
|
|
380
|
+
request_id=None,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
assert result["success"] is True
|
|
384
|
+
|
|
385
|
+
@pytest.mark.asyncio
|
|
386
|
+
async def test_update_journal_summary_validation_error(self, setup_managers):
|
|
387
|
+
"""Test update_journal validation error for summary"""
|
|
388
|
+
with patch(
|
|
389
|
+
"chronos_mcp.tools.journals.InputValidator.validate_text_field"
|
|
390
|
+
) as mock_validate:
|
|
391
|
+
mock_validate.side_effect = ValidationError("Summary invalid")
|
|
392
|
+
|
|
393
|
+
result = await update_journal.fn(
|
|
394
|
+
calendar_uid="cal-123",
|
|
395
|
+
journal_uid="journal-123",
|
|
396
|
+
summary="Invalid summary",
|
|
397
|
+
description=None,
|
|
398
|
+
entry_date=None,
|
|
399
|
+
account=None,
|
|
400
|
+
request_id=None,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
assert result["success"] is False
|
|
404
|
+
assert "Summary invalid" in result["error"]
|
|
405
|
+
|
|
406
|
+
@pytest.mark.asyncio
|
|
407
|
+
async def test_update_journal_description_validation_error(self, setup_managers):
|
|
408
|
+
"""Test update_journal validation error for description"""
|
|
409
|
+
with patch(
|
|
410
|
+
"chronos_mcp.tools.journals.InputValidator.validate_text_field"
|
|
411
|
+
) as mock_validate:
|
|
412
|
+
mock_validate.side_effect = ValidationError("Description invalid")
|
|
413
|
+
|
|
414
|
+
result = await update_journal.fn(
|
|
415
|
+
calendar_uid="cal-123",
|
|
416
|
+
journal_uid="journal-123",
|
|
417
|
+
summary=None,
|
|
418
|
+
description="Invalid description",
|
|
419
|
+
entry_date=None,
|
|
420
|
+
account=None,
|
|
421
|
+
request_id=None,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
assert result["success"] is False
|
|
425
|
+
assert "Description invalid" in result["error"]
|
|
426
|
+
|
|
427
|
+
@pytest.mark.asyncio
|
|
428
|
+
async def test_update_journal_entry_date_none_in_response(self, setup_managers):
|
|
429
|
+
"""Test update_journal with None entry date in response"""
|
|
430
|
+
sample_journal = Mock()
|
|
431
|
+
sample_journal.uid = "journal-123"
|
|
432
|
+
sample_journal.summary = "Test Journal"
|
|
433
|
+
sample_journal.description = "Test content"
|
|
434
|
+
sample_journal.dtstart = None # No entry date
|
|
435
|
+
sample_journal.related_to = []
|
|
436
|
+
|
|
437
|
+
_managers["journal_manager"].update_journal.return_value = sample_journal
|
|
438
|
+
|
|
439
|
+
result = await update_journal.fn(
|
|
440
|
+
calendar_uid="cal-123",
|
|
441
|
+
journal_uid="journal-123",
|
|
442
|
+
summary="Updated",
|
|
443
|
+
description=None,
|
|
444
|
+
entry_date=None,
|
|
445
|
+
account=None,
|
|
446
|
+
request_id=None,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
assert result["success"] is True
|
|
450
|
+
assert result["journal"]["entry_date"] is None
|
|
451
|
+
|
|
452
|
+
@pytest.mark.asyncio
|
|
453
|
+
async def test_update_journal_malformed_entry_date(self, setup_managers):
|
|
454
|
+
"""Test update_journal with malformed entry date triggering parse_datetime error"""
|
|
455
|
+
with patch("chronos_mcp.tools.journals.parse_datetime") as mock_parse:
|
|
456
|
+
mock_parse.side_effect = ValueError("Invalid date format")
|
|
457
|
+
|
|
458
|
+
result = await update_journal.fn(
|
|
459
|
+
calendar_uid="cal-123",
|
|
460
|
+
journal_uid="journal-123",
|
|
461
|
+
summary=None,
|
|
462
|
+
description=None,
|
|
463
|
+
entry_date="invalid-date",
|
|
464
|
+
account=None,
|
|
465
|
+
request_id=None,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
assert result["success"] is False
|
|
469
|
+
|
|
470
|
+
# DELETE_JOURNAL TOOL TESTS (uses @handle_tool_errors decorator)
|
|
471
|
+
|
|
472
|
+
@pytest.mark.asyncio
|
|
473
|
+
async def test_delete_journal_success(self, setup_managers):
|
|
474
|
+
"""Test delete_journal successful execution"""
|
|
475
|
+
_managers["journal_manager"].delete_journal.return_value = True
|
|
476
|
+
|
|
477
|
+
result = await delete_journal.fn(
|
|
478
|
+
calendar_uid="cal-123",
|
|
479
|
+
journal_uid="journal-123",
|
|
480
|
+
account=None,
|
|
481
|
+
request_id=None,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
assert result["success"] is True
|
|
485
|
+
assert "deleted successfully" in result["message"]
|
|
486
|
+
assert "request_id" in result
|
|
487
|
+
|
|
488
|
+
@pytest.mark.asyncio
|
|
489
|
+
async def test_delete_journal_with_account(self, setup_managers):
|
|
490
|
+
"""Test delete_journal with account parameter"""
|
|
491
|
+
_managers["journal_manager"].delete_journal.return_value = True
|
|
492
|
+
|
|
493
|
+
result = await delete_journal.fn(
|
|
494
|
+
calendar_uid="cal-123",
|
|
495
|
+
journal_uid="journal-123",
|
|
496
|
+
account="test_account",
|
|
497
|
+
request_id=None,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
_managers["journal_manager"].delete_journal.assert_called_once_with(
|
|
501
|
+
calendar_uid="cal-123",
|
|
502
|
+
journal_uid="journal-123",
|
|
503
|
+
account_alias="test_account",
|
|
504
|
+
request_id=result["request_id"],
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# REGISTER_JOURNAL_TOOLS TESTS
|
|
508
|
+
|
|
509
|
+
def test_register_journal_tools(self, mock_managers, setup_managers):
|
|
510
|
+
"""Test register_journal_tools function"""
|
|
511
|
+
mock_mcp = Mock()
|
|
512
|
+
|
|
513
|
+
register_journal_tools(mock_mcp, mock_managers)
|
|
514
|
+
|
|
515
|
+
# Verify managers were updated - strict equality now works with clean state from fixture
|
|
516
|
+
assert _managers == mock_managers
|
|
517
|
+
|
|
518
|
+
# Verify all tools were registered
|
|
519
|
+
assert mock_mcp.tool.call_count == 4
|
|
520
|
+
|
|
521
|
+
# Verify specific tools were registered
|
|
522
|
+
calls = [call[0][0] for call in mock_mcp.tool.call_args_list]
|
|
523
|
+
assert create_journal in calls
|
|
524
|
+
assert list_journals in calls
|
|
525
|
+
assert update_journal in calls
|
|
526
|
+
assert delete_journal in calls
|
|
527
|
+
|
|
528
|
+
# FUNCTION ATTRIBUTE TESTS
|
|
529
|
+
|
|
530
|
+
def test_function_attributes_exist(self):
|
|
531
|
+
"""Test that .fn attributes exist for backwards compatibility"""
|
|
532
|
+
assert hasattr(create_journal, "fn")
|
|
533
|
+
assert hasattr(list_journals, "fn")
|
|
534
|
+
assert hasattr(update_journal, "fn")
|
|
535
|
+
assert hasattr(delete_journal, "fn")
|
|
536
|
+
|
|
537
|
+
assert create_journal.fn == create_journal
|
|
538
|
+
assert list_journals.fn == list_journals
|
|
539
|
+
assert update_journal.fn == update_journal
|
|
540
|
+
assert delete_journal.fn == delete_journal
|
|
541
|
+
|
|
542
|
+
# EDGE CASES AND DEFENSIVE PROGRAMMING
|
|
543
|
+
|
|
544
|
+
@pytest.mark.asyncio
|
|
545
|
+
async def test_create_journal_empty_summary(self, setup_managers):
|
|
546
|
+
"""Test create_journal with empty summary"""
|
|
547
|
+
with patch(
|
|
548
|
+
"chronos_mcp.tools.journals.InputValidator.validate_text_field"
|
|
549
|
+
) as mock_validate:
|
|
550
|
+
mock_validate.side_effect = ValidationError("Summary is required")
|
|
551
|
+
|
|
552
|
+
result = await create_journal.fn(
|
|
553
|
+
calendar_uid="cal-123",
|
|
554
|
+
summary="",
|
|
555
|
+
description=None,
|
|
556
|
+
entry_date=None,
|
|
557
|
+
related_to=None,
|
|
558
|
+
account=None,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
assert result["success"] is False
|
|
562
|
+
assert "Summary is required" in result["error"]
|
|
563
|
+
|
|
564
|
+
@pytest.mark.asyncio
|
|
565
|
+
async def test_list_journals_limit_none(self, setup_managers, sample_journal):
|
|
566
|
+
"""Test list_journals with limit as None"""
|
|
567
|
+
_managers["journal_manager"].list_journals.return_value = [sample_journal]
|
|
568
|
+
|
|
569
|
+
result = await list_journals.fn(
|
|
570
|
+
calendar_uid="cal-123", account=None, limit=None
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
assert len(result["journals"]) == 1
|
|
574
|
+
_managers["journal_manager"].list_journals.assert_called_once_with(
|
|
575
|
+
calendar_uid="cal-123", limit=None, account_alias=None
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
@pytest.mark.asyncio
|
|
579
|
+
async def test_managers_not_initialized(self):
|
|
580
|
+
"""Test behavior when _managers is not properly initialized"""
|
|
581
|
+
# Clear managers to simulate uninitialized state
|
|
582
|
+
original = _managers.copy()
|
|
583
|
+
_managers.clear()
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
result = await create_journal.fn(
|
|
587
|
+
calendar_uid="cal-123",
|
|
588
|
+
summary="Test Journal",
|
|
589
|
+
description=None,
|
|
590
|
+
entry_date=None,
|
|
591
|
+
related_to=None,
|
|
592
|
+
account=None,
|
|
593
|
+
)
|
|
594
|
+
# Should get an error response, not an exception
|
|
595
|
+
assert result["success"] is False
|
|
596
|
+
assert "Failed to create journal" in result["error"]
|
|
597
|
+
finally:
|
|
598
|
+
_managers.update(original)
|
|
599
|
+
|
|
600
|
+
@pytest.mark.asyncio
|
|
601
|
+
async def test_create_journal_empty_description_not_validated(
|
|
602
|
+
self, setup_managers, sample_journal
|
|
603
|
+
):
|
|
604
|
+
"""Test create_journal with empty description (should not be validated)"""
|
|
605
|
+
_managers["journal_manager"].create_journal.return_value = sample_journal
|
|
606
|
+
|
|
607
|
+
result = await create_journal.fn(
|
|
608
|
+
calendar_uid="cal-123",
|
|
609
|
+
summary="Test Journal",
|
|
610
|
+
description="", # Empty description should be ignored
|
|
611
|
+
entry_date=None,
|
|
612
|
+
related_to=None,
|
|
613
|
+
account=None,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Empty description should not trigger validation
|
|
617
|
+
assert result["success"] is True
|