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,234 @@
1
+ """
2
+ Security-focused tests for URL validation
3
+ """
4
+
5
+ import pytest
6
+
7
+ from chronos_mcp.exceptions import ValidationError
8
+ from chronos_mcp.validation import InputValidator
9
+
10
+
11
+ class TestUrlValidationSecurity:
12
+ """Test security aspects of URL validation"""
13
+
14
+ def test_https_only_enforcement(self):
15
+ """Test that only HTTPS URLs are allowed"""
16
+ validator = InputValidator()
17
+
18
+ # Valid HTTPS URLs should pass
19
+ valid_urls = [
20
+ "https://caldav.example.com/",
21
+ "https://calendar.company.org/caldav/",
22
+ "https://subdomain.example.co.uk/path/to/caldav",
23
+ "https://192.168.1.100:8443/caldav",
24
+ "https://example.com:443/",
25
+ ]
26
+
27
+ for url in valid_urls:
28
+ assert validator.PATTERNS["url"].match(
29
+ url
30
+ ), f"Valid HTTPS URL should match: {url}"
31
+
32
+ def test_http_urls_rejected(self):
33
+ """Test that HTTP URLs are rejected"""
34
+ validator = InputValidator()
35
+
36
+ # HTTP URLs should be rejected
37
+ invalid_urls = [
38
+ "http://caldav.example.com/",
39
+ "http://calendar.company.org/caldav/",
40
+ "http://example.com/path",
41
+ "http://192.168.1.100:8080/caldav",
42
+ "http://localhost:8080/",
43
+ ]
44
+
45
+ for url in invalid_urls:
46
+ assert not validator.PATTERNS["url"].match(
47
+ url
48
+ ), f"HTTP URL should be rejected: {url}"
49
+
50
+ def test_malicious_url_schemes_rejected(self):
51
+ """Test that malicious URL schemes are rejected"""
52
+ validator = InputValidator()
53
+
54
+ # Malicious schemes should be rejected
55
+ malicious_urls = [
56
+ "javascript:alert('xss')",
57
+ "data:text/html,<script>alert('xss')</script>",
58
+ "ftp://malicious.com/",
59
+ "file:///etc/passwd",
60
+ "gopher://evil.com/",
61
+ "ldap://attacker.com/",
62
+ "mailto:victim@example.com",
63
+ "tel:+1234567890",
64
+ ]
65
+
66
+ for url in malicious_urls:
67
+ assert not validator.PATTERNS["url"].match(
68
+ url
69
+ ), f"Malicious URL should be rejected: {url}"
70
+
71
+ def test_url_injection_attempts_rejected(self):
72
+ """Test that URL injection attempts are rejected"""
73
+ validator = InputValidator()
74
+
75
+ # URL injection attempts should be rejected
76
+ injection_urls = [
77
+ "https://evil.com@example.com/", # Credential phishing - @ not allowed
78
+ "https://example .com/path", # Space in domain
79
+ "https://example.com:99999/path", # Invalid port
80
+ ]
81
+
82
+ for url in injection_urls:
83
+ assert not validator.PATTERNS["url"].match(
84
+ url
85
+ ), f"Dangerous URL should be rejected: {url}"
86
+
87
+ # These URLs will match our pattern but contain potentially dangerous content
88
+ # They should be caught by other validation layers (like dangerous pattern detection)
89
+ potentially_dangerous_but_valid_format = [
90
+ "https://example.com/path?param=javascript:alert(1)",
91
+ "https://example.com/path#javascript:alert(1)",
92
+ "https://example.com/../../../etc/passwd",
93
+ "https://example.com/path?redirect=http://evil.com",
94
+ ]
95
+
96
+ # These match the URL format but should be caught by dangerous pattern validation
97
+ for url in potentially_dangerous_but_valid_format:
98
+ # The URL pattern itself might match (that's OK), but dangerous content
99
+ # should be caught by the DANGEROUS_PATTERNS in validate_text_field
100
+ result = validator.PATTERNS["url"].match(url)
101
+ # This is acceptable - the URL format is valid, but content filtering should catch it
102
+
103
+ def test_localhost_and_private_ips_pattern_matching(self):
104
+ """Test that localhost and private IPs match the URL pattern (but may be blocked by validate_url)"""
105
+ validator = InputValidator()
106
+
107
+ # These match the URL pattern format (for backward compatibility)
108
+ # but are now blocked by default in validate_url() for SSRF protection
109
+ local_urls = [
110
+ "https://localhost:8443/caldav",
111
+ "https://127.0.0.1:8443/caldav",
112
+ "https://192.168.1.100:8443/caldav",
113
+ "https://10.0.0.50:8443/caldav",
114
+ "https://172.16.0.10:8443/caldav",
115
+ ]
116
+
117
+ for url in local_urls:
118
+ # Pattern still matches for backward compatibility
119
+ assert validator.PATTERNS["url"].match(
120
+ url
121
+ ), f"Local/private URL pattern should match: {url}"
122
+
123
+ # But validate_url blocks them by default (SSRF protection)
124
+ with pytest.raises(ValidationError):
125
+ validator.validate_url(url)
126
+
127
+ # Unless explicitly allowed
128
+ result = validator.validate_url(url, allow_private_ips=True)
129
+ assert result == url
130
+
131
+ def test_url_with_unusual_ports(self):
132
+ """Test URLs with unusual but valid ports"""
133
+ validator = InputValidator()
134
+
135
+ urls_with_ports = [
136
+ "https://example.com:8443/caldav",
137
+ "https://example.com:9443/caldav",
138
+ "https://example.com:443/caldav", # Standard HTTPS port
139
+ "https://example.com:8080/caldav", # Common alternative
140
+ ]
141
+
142
+ for url in urls_with_ports:
143
+ assert validator.PATTERNS["url"].match(
144
+ url
145
+ ), f"URL with port should be allowed: {url}"
146
+
147
+ def test_empty_and_malformed_urls(self):
148
+ """Test handling of empty and malformed URLs"""
149
+ validator = InputValidator()
150
+
151
+ malformed_urls = [
152
+ "",
153
+ "not-a-url",
154
+ "://missing-scheme.com",
155
+ "https://",
156
+ "https:///path-without-domain",
157
+ "https://.com/",
158
+ "https://example.",
159
+ "https://example .com/", # Space in domain
160
+ "https://example.com:abc/", # Invalid port
161
+ "https://example.com:0/", # Port 0 not allowed
162
+ ]
163
+
164
+ for url in malformed_urls:
165
+ assert not validator.PATTERNS["url"].match(
166
+ url
167
+ ), f"Malformed URL should be rejected: {url}"
168
+
169
+ # These might match our pattern but are edge cases we should handle
170
+ edge_cases = [
171
+ "https://domain-without-tld", # This will match as single hostname
172
+ "https://example..com/", # This might match due to our regex
173
+ ]
174
+
175
+ # Note: Some edge cases might pass the regex but should be caught by other validation
176
+
177
+ def test_very_long_urls(self):
178
+ """Test handling of extremely long URLs"""
179
+ validator = InputValidator()
180
+
181
+ # Create a very long but otherwise valid URL
182
+ long_path = "a" * 1000
183
+ long_url = f"https://example.com/{long_path}"
184
+
185
+ # The pattern itself should match, but length validation should happen elsewhere
186
+ # This tests that the regex doesn't break with long inputs
187
+ result = validator.PATTERNS["url"].match(long_url)
188
+ assert (
189
+ result is not None
190
+ ), "Long URL should match pattern (length validation is separate)"
191
+
192
+ def test_unicode_domains_handled(self):
193
+ """Test handling of internationalized domain names"""
194
+ validator = InputValidator()
195
+
196
+ # These might be legitimate but should be handled carefully
197
+ unicode_domains = [
198
+ "https://xn--example-9ua.com/caldav", # Punycode encoded
199
+ "https://bücher.example.com/caldav", # Direct Unicode (might not match our pattern)
200
+ ]
201
+
202
+ # Our current pattern is ASCII-only, which is actually a security feature
203
+ # Unicode domains should be punycode-encoded first
204
+ assert validator.PATTERNS["url"].match(
205
+ unicode_domains[0]
206
+ ), "Punycode domain should be allowed"
207
+ assert not validator.PATTERNS["url"].match(
208
+ unicode_domains[1]
209
+ ), "Direct Unicode should be rejected (security feature)"
210
+
211
+ def test_case_sensitivity(self):
212
+ """Test that URL scheme matching is case-sensitive for security"""
213
+ validator = InputValidator()
214
+
215
+ # Only lowercase 'https' should be allowed
216
+ case_variants = [
217
+ "HTTPS://example.com/caldav",
218
+ "Https://example.com/caldav",
219
+ "HTTPs://example.com/caldav",
220
+ "https://EXAMPLE.COM/caldav", # Domain case shouldn't matter
221
+ ]
222
+
223
+ assert not validator.PATTERNS["url"].match(
224
+ case_variants[0]
225
+ ), "Uppercase HTTPS should be rejected"
226
+ assert not validator.PATTERNS["url"].match(
227
+ case_variants[1]
228
+ ), "Mixed case Https should be rejected"
229
+ assert not validator.PATTERNS["url"].match(
230
+ case_variants[2]
231
+ ), "Mixed case HTTPs should be rejected"
232
+ assert validator.PATTERNS["url"].match(
233
+ case_variants[3]
234
+ ), "Domain case should not matter"
@@ -0,0 +1,180 @@
1
+ """
2
+ Unit tests for utility functions
3
+ """
4
+
5
+ from datetime import date, datetime, timezone
6
+
7
+ import pytest
8
+ import pytz
9
+
10
+ from chronos_mcp.utils import (
11
+ create_ical_event,
12
+ datetime_to_ical,
13
+ ical_to_datetime,
14
+ parse_datetime,
15
+ )
16
+
17
+
18
+ class TestParseDatetime:
19
+ """Test parse_datetime function"""
20
+
21
+ def test_parse_datetime_object(self):
22
+ """Test parsing when input is already a datetime"""
23
+ dt = datetime(2025, 7, 10, 14, 0, tzinfo=timezone.utc)
24
+ result = parse_datetime(dt)
25
+ assert result == dt
26
+
27
+ def test_parse_iso_string(self):
28
+ """Test parsing ISO format string"""
29
+ result = parse_datetime("2025-07-10T14:00:00Z")
30
+ expected = datetime(2025, 7, 10, 14, 0, tzinfo=timezone.utc)
31
+ assert result == expected
32
+
33
+ def test_parse_naive_datetime(self):
34
+ """Test parsing datetime without timezone"""
35
+ result = parse_datetime("2025-07-10 14:00:00")
36
+ assert result.tzinfo == timezone.utc
37
+ assert result.year == 2025
38
+ assert result.month == 7
39
+ assert result.day == 10
40
+ assert result.hour == 14
41
+
42
+ def test_parse_various_formats(self):
43
+ """Test parsing various datetime formats"""
44
+ formats = [
45
+ "2025-07-10",
46
+ "07/10/2025",
47
+ "July 10, 2025",
48
+ "2025-07-10T14:00:00+00:00",
49
+ "2025-07-10T14:00:00-05:00",
50
+ ]
51
+
52
+ for fmt in formats:
53
+ result = parse_datetime(fmt)
54
+ assert isinstance(result, datetime)
55
+ assert result.tzinfo is not None
56
+
57
+ def test_parse_invalid_format(self):
58
+ """Test parsing invalid datetime format"""
59
+ with pytest.raises(ValueError, match="Invalid datetime format"):
60
+ parse_datetime("not a date")
61
+
62
+
63
+ class TestDatetimeToIcal:
64
+ """Test datetime_to_ical function"""
65
+
66
+ def test_datetime_to_ical_regular(self):
67
+ """Test converting regular datetime to iCal format"""
68
+ dt = datetime(2025, 7, 10, 14, 30, 45, tzinfo=timezone.utc)
69
+ result = datetime_to_ical(dt)
70
+ assert result == "20250710T143045Z"
71
+
72
+ def test_datetime_to_ical_all_day(self):
73
+ """Test converting all-day event to iCal format"""
74
+ dt = datetime(2025, 7, 10, 0, 0, 0, tzinfo=timezone.utc)
75
+ result = datetime_to_ical(dt, all_day=True)
76
+ assert result == "20250710"
77
+
78
+ def test_datetime_to_ical_naive(self):
79
+ """Test converting naive datetime (assumes UTC)"""
80
+ dt = datetime(2025, 7, 10, 14, 30, 45)
81
+ result = datetime_to_ical(dt)
82
+ assert result == "20250710T143045Z"
83
+
84
+ def test_datetime_to_ical_other_timezone(self):
85
+ """Test converting datetime in non-UTC timezone"""
86
+ eastern = pytz.timezone("US/Eastern")
87
+ dt = eastern.localize(datetime(2025, 7, 10, 14, 30, 45))
88
+ result = datetime_to_ical(dt)
89
+ # Should be converted to UTC
90
+ assert result.endswith("Z")
91
+ assert "1830" in result or "1930" in result # Accounts for DST
92
+
93
+
94
+ class TestIcalToDatetime:
95
+ """Test ical_to_datetime function"""
96
+
97
+ def test_ical_to_datetime_with_dt_attribute(self):
98
+ """Test converting iCal object with dt attribute"""
99
+ from icalendar import vDatetime
100
+
101
+ ical_dt = vDatetime.from_ical("20250710T143045Z")
102
+ result = ical_to_datetime(ical_dt)
103
+ expected = datetime(2025, 7, 10, 14, 30, 45, tzinfo=timezone.utc)
104
+ assert result == expected
105
+
106
+ def test_ical_to_datetime_direct_datetime(self):
107
+ """Test converting direct datetime object"""
108
+ dt = datetime(2025, 7, 10, 14, 30, 45, tzinfo=timezone.utc)
109
+ result = ical_to_datetime(dt)
110
+ assert result == dt
111
+
112
+ def test_ical_to_datetime_date_only(self):
113
+ """Test converting date-only (all-day event)"""
114
+ dt = date(2025, 7, 10)
115
+ result = ical_to_datetime(dt)
116
+ expected = datetime(2025, 7, 10, 0, 0, 0, tzinfo=timezone.utc)
117
+ assert result == expected
118
+
119
+ def test_ical_to_datetime_naive(self):
120
+ """Test converting naive datetime"""
121
+ dt = datetime(2025, 7, 10, 14, 30, 45)
122
+ result = ical_to_datetime(dt)
123
+ assert result.tzinfo == timezone.utc
124
+ assert result.replace(tzinfo=None) == dt
125
+
126
+
127
+ class TestCreateIcalEvent:
128
+ """Test create_ical_event function"""
129
+
130
+ def test_create_ical_event_minimal(self):
131
+ """Test creating event with minimal data"""
132
+ event_data = {
133
+ "uid": "test-123",
134
+ "summary": "Test Event",
135
+ "start": datetime(2025, 7, 10, 14, 0, tzinfo=timezone.utc),
136
+ "end": datetime(2025, 7, 10, 15, 0, tzinfo=timezone.utc),
137
+ }
138
+
139
+ event = create_ical_event(event_data)
140
+
141
+ assert event["uid"] == "test-123"
142
+ assert event["summary"] == "Test Event"
143
+ assert event["dtstart"].dt == event_data["start"]
144
+ assert event["dtend"].dt == event_data["end"]
145
+
146
+ def test_create_ical_event_full(self):
147
+ """Test creating event with all optional fields"""
148
+ event_data = {
149
+ "uid": "test-456",
150
+ "summary": "Full Event",
151
+ "start": datetime(2025, 7, 10, 14, 0, tzinfo=timezone.utc),
152
+ "end": datetime(2025, 7, 10, 15, 0, tzinfo=timezone.utc),
153
+ "description": "This is a test event",
154
+ "location": "Conference Room A",
155
+ "status": "CONFIRMED",
156
+ }
157
+
158
+ event = create_ical_event(event_data)
159
+
160
+ assert event["uid"] == "test-456"
161
+ assert event["summary"] == "Full Event"
162
+ assert event["description"] == "This is a test event"
163
+ assert event["location"] == "Conference Room A"
164
+ assert event["status"] == "CONFIRMED"
165
+
166
+ def test_create_ical_event_missing_optional(self):
167
+ """Test creating event without optional fields"""
168
+ event_data = {
169
+ "uid": "test-789",
170
+ "summary": "Basic Event",
171
+ "start": datetime(2025, 7, 10, 14, 0, tzinfo=timezone.utc),
172
+ "end": datetime(2025, 7, 10, 15, 0, tzinfo=timezone.utc),
173
+ }
174
+
175
+ event = create_ical_event(event_data)
176
+
177
+ # Optional fields should not be present
178
+ assert "description" not in event
179
+ assert "location" not in event
180
+ assert "status" not in event