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,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"
|
tests/unit/test_utils.py
ADDED
|
@@ -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
|