🐛(data) remove Calendar and CalendarShare models

The only source of truth for those is now in the caldav server
This commit is contained in:
Sylvain Zimmer
2026-02-09 20:48:11 +01:00
parent 3a0f64e791
commit 3051100f8a
24 changed files with 408 additions and 883 deletions

View File

@@ -500,7 +500,7 @@ def test_authentication_session_tokens(
status=200,
)
with django_assert_num_queries(7):
with django_assert_num_queries(5):
user = klass.authenticate(
request,
code="test-code",

View File

@@ -95,7 +95,7 @@ class TestCalDAVScheduling:
# Create calendar for organizer
service = CalendarService()
calendar = service.create_calendar(
caldav_path = service.create_calendar(
organizer, name="Test Calendar", color="#ff0000"
)
@@ -126,7 +126,7 @@ class TestCalDAVScheduling:
try:
# Create an event with an attendee
client = service.caldav._get_client(organizer) # pylint: disable=protected-access
calendar_url = f"{settings.CALDAV_URL}{calendar.caldav_path}"
calendar_url = f"{settings.CALDAV_URL}{caldav_path}"
# Add custom callback URL header to the client
# The CalDAV server will use this URL for the callback

View File

@@ -59,12 +59,10 @@ class TestCalDAVClient:
user = factories.UserFactory(email="test@example.com")
service = CalendarService()
# Create a calendar
calendar = service.create_calendar(user, name="My Calendar", color="#ff0000")
# Create a calendar — returns caldav_path string
caldav_path = service.create_calendar(user, name="My Calendar", color="#ff0000")
# Verify calendar was created
assert calendar is not None
assert calendar.owner == user
assert calendar.name == "My Calendar"
assert calendar.color == "#ff0000"
assert calendar.caldav_path is not None
# Verify caldav_path was returned
assert caldav_path is not None
assert isinstance(caldav_path, str)
assert "calendars/" in caldav_path

View File

@@ -1,6 +1,7 @@
"""Tests for the ICS import events feature.""" # pylint: disable=too-many-lines
import json
import uuid
from datetime import datetime
from datetime import timezone as dt_tz
from unittest.mock import MagicMock, patch
@@ -244,6 +245,11 @@ END:VEVENT
END:VCALENDAR"""
def _make_caldav_path(user):
"""Build a caldav_path string for a user (test helper)."""
return f"/calendars/{user.email}/{uuid.uuid4()}/"
def _make_sabredav_response( # noqa: PLR0913 # pylint: disable=too-many-arguments,too-many-positional-arguments
status_code=200,
total_events=0,
@@ -278,10 +284,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_SINGLE_EVENT)
result = service.import_events(user, caldav_path, ICS_SINGLE_EVENT)
assert result.total_events == 1
assert result.imported_count == 1
@@ -301,10 +307,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_MULTIPLE_EVENTS)
result = service.import_events(user, caldav_path, ICS_MULTIPLE_EVENTS)
assert result.total_events == 3
assert result.imported_count == 3
@@ -321,10 +327,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_EMPTY)
result = service.import_events(user, caldav_path, ICS_EMPTY)
assert result.total_events == 0
assert result.imported_count == 0
@@ -340,10 +346,10 @@ class TestICSImportService:
mock_post.return_value.text = '{"error": "Failed to parse ICS file"}'
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_INVALID)
result = service.import_events(user, caldav_path, ICS_INVALID)
assert result.imported_count == 0
assert len(result.errors) >= 1
@@ -356,10 +362,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_WITH_TIMEZONE)
result = service.import_events(user, caldav_path, ICS_WITH_TIMEZONE)
assert result.total_events == 1
assert result.imported_count == 1
@@ -386,10 +392,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_MULTIPLE_EVENTS)
result = service.import_events(user, caldav_path, ICS_MULTIPLE_EVENTS)
assert result.total_events == 3
assert result.imported_count == 2
@@ -406,10 +412,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_ALL_DAY_EVENT)
result = service.import_events(user, caldav_path, ICS_ALL_DAY_EVENT)
assert result.total_events == 1
assert result.imported_count == 1
@@ -422,10 +428,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_VALARM_NO_ACTION)
result = service.import_events(user, caldav_path, ICS_VALARM_NO_ACTION)
assert result.total_events == 1
assert result.imported_count == 1
@@ -438,10 +444,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_RECURRING_WITH_EXCEPTION)
result = service.import_events(user, caldav_path, ICS_RECURRING_WITH_EXCEPTION)
# Two VEVENTs with same UID = one logical event
assert result.total_events == 1
@@ -464,10 +470,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_NO_DTSTART)
result = service.import_events(user, caldav_path, ICS_NO_DTSTART)
assert result.total_events == 1
assert result.imported_count == 0
@@ -476,20 +482,20 @@ class TestICSImportService:
@patch("core.services.import_service.requests.post")
def test_import_passes_calendar_path(self, mock_post):
"""The import URL should include the calendar's caldav_path."""
"""The import URL should include the caldav_path."""
mock_post.return_value = _make_sabredav_response(
total_events=1, imported_count=1
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
service.import_events(user, calendar, ICS_SINGLE_EVENT)
service.import_events(user, caldav_path, ICS_SINGLE_EVENT)
call_args = mock_post.call_args
url = call_args.args[0] if call_args.args else call_args.kwargs.get("url", "")
assert calendar.caldav_path in url
assert caldav_path in url
assert "?import" in url
@patch("core.services.import_service.requests.post")
@@ -500,10 +506,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
service.import_events(user, calendar, ICS_SINGLE_EVENT)
service.import_events(user, caldav_path, ICS_SINGLE_EVENT)
call_kwargs = mock_post.call_args.kwargs
headers = call_kwargs["headers"]
@@ -524,10 +530,10 @@ class TestICSImportService:
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_MULTIPLE_EVENTS)
result = service.import_events(user, caldav_path, ICS_MULTIPLE_EVENTS)
assert result.total_events == 3
assert result.imported_count == 1
@@ -541,10 +547,10 @@ class TestICSImportService:
mock_post.side_effect = req.ConnectionError("Connection refused")
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
caldav_path = _make_caldav_path(user)
service = ICSImportService()
result = service.import_events(user, calendar, ICS_SINGLE_EVENT)
result = service.import_events(user, caldav_path, ICS_SINGLE_EVENT)
assert result.imported_count == 0
assert len(result.errors) >= 1
@@ -553,23 +559,19 @@ class TestICSImportService:
class TestImportEventsAPI:
"""API endpoint tests for the import_events action."""
def _get_url(self, calendar_id):
return f"/api/v1.0/calendars/{calendar_id}/import_events/"
IMPORT_URL = "/api/v1.0/calendars/import-events/"
def test_import_events_requires_authentication(self):
"""Unauthenticated requests should be rejected."""
calendar = factories.CalendarFactory()
client = APIClient()
response = client.post(self._get_url(calendar.id))
response = client.post(self.IMPORT_URL)
assert response.status_code == 401
def test_import_events_forbidden_for_non_owner(self):
"""Non-owners should not be able to access the calendar."""
owner = factories.UserFactory()
other_user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=owner)
def test_import_events_forbidden_for_wrong_user(self):
"""Users cannot import to a calendar they don't own."""
owner = factories.UserFactory(email="owner@example.com")
other_user = factories.UserFactory(email="other@example.com")
caldav_path = f"/calendars/{owner.email}/some-uuid/"
client = APIClient()
client.force_login(other_user)
@@ -578,29 +580,45 @@ class TestImportEventsAPI:
"events.ics", ICS_SINGLE_EVENT, content_type="text/calendar"
)
response = client.post(
self._get_url(calendar.id), {"file": ics_file}, format="multipart"
self.IMPORT_URL,
{"file": ics_file, "caldav_path": caldav_path},
format="multipart",
)
assert response.status_code == 403
# Calendar not in queryset for non-owner, so 404 (not 403)
assert response.status_code == 404
def test_import_events_missing_caldav_path(self):
"""Request without caldav_path should return 400."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
ics_file = SimpleUploadedFile(
"events.ics", ICS_SINGLE_EVENT, content_type="text/calendar"
)
response = client.post(self.IMPORT_URL, {"file": ics_file}, format="multipart")
assert response.status_code == 400
assert "caldav_path" in response.json()["error"]
def test_import_events_missing_file(self):
"""Request without a file should return 400."""
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
user = factories.UserFactory(email="nofile@example.com")
caldav_path = f"/calendars/{user.email}/some-uuid/"
client = APIClient()
client.force_login(user)
response = client.post(self._get_url(calendar.id), format="multipart")
response = client.post(
self.IMPORT_URL,
{"caldav_path": caldav_path},
format="multipart",
)
assert response.status_code == 400
assert "No file provided" in response.json()["error"]
def test_import_events_file_too_large(self):
"""Files exceeding MAX_FILE_SIZE should be rejected."""
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
user = factories.UserFactory(email="largefile@example.com")
caldav_path = f"/calendars/{user.email}/some-uuid/"
client = APIClient()
client.force_login(user)
@@ -611,9 +629,10 @@ class TestImportEventsAPI:
content_type="text/calendar",
)
response = client.post(
self._get_url(calendar.id), {"file": large_file}, format="multipart"
self.IMPORT_URL,
{"file": large_file, "caldav_path": caldav_path},
format="multipart",
)
assert response.status_code == 400
assert "too large" in response.json()["error"]
@@ -628,8 +647,8 @@ class TestImportEventsAPI:
errors=[],
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
user = factories.UserFactory(email="success@example.com")
caldav_path = f"/calendars/{user.email}/some-uuid/"
client = APIClient()
client.force_login(user)
@@ -638,7 +657,9 @@ class TestImportEventsAPI:
"events.ics", ICS_MULTIPLE_EVENTS, content_type="text/calendar"
)
response = client.post(
self._get_url(calendar.id), {"file": ics_file}, format="multipart"
self.IMPORT_URL,
{"file": ics_file, "caldav_path": caldav_path},
format="multipart",
)
assert response.status_code == 200
@@ -659,8 +680,8 @@ class TestImportEventsAPI:
errors=["Planning session"],
)
user = factories.UserFactory()
calendar = factories.CalendarFactory(owner=user)
user = factories.UserFactory(email="partial@example.com")
caldav_path = f"/calendars/{user.email}/some-uuid/"
client = APIClient()
client.force_login(user)
@@ -669,7 +690,9 @@ class TestImportEventsAPI:
"events.ics", ICS_MULTIPLE_EVENTS, content_type="text/calendar"
)
response = client.post(
self._get_url(calendar.id), {"file": ics_file}, format="multipart"
self.IMPORT_URL,
{"file": ics_file, "caldav_path": caldav_path},
format="multipart",
)
assert response.status_code == 200
@@ -688,17 +711,17 @@ class TestImportEventsE2E:
"""End-to-end tests that import ICS events through the real SabreDAV server."""
def _create_calendar(self, user):
"""Create a real calendar in both Django and SabreDAV."""
"""Create a real calendar in SabreDAV. Returns the caldav_path."""
service = CalendarService()
return service.create_calendar(user, name="Import Test", color="#3174ad")
def test_import_single_event_e2e(self):
"""Import a single event and verify it exists in SabreDAV."""
user = factories.UserFactory(email="import-single@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(user, calendar, ICS_SINGLE_EVENT)
result = import_service.import_events(user, caldav_path, ICS_SINGLE_EVENT)
assert result.total_events == 1
assert result.imported_count == 1
@@ -709,7 +732,7 @@ class TestImportEventsE2E:
caldav = CalDAVClient()
events = caldav.get_events(
user,
calendar.caldav_path,
caldav_path,
start=datetime(2026, 2, 10, tzinfo=dt_tz.utc),
end=datetime(2026, 2, 11, tzinfo=dt_tz.utc),
)
@@ -720,10 +743,10 @@ class TestImportEventsE2E:
def test_import_multiple_events_e2e(self):
"""Import multiple events and verify they all exist in SabreDAV."""
user = factories.UserFactory(email="import-multi@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(user, calendar, ICS_MULTIPLE_EVENTS)
result = import_service.import_events(user, caldav_path, ICS_MULTIPLE_EVENTS)
assert result.total_events == 3
assert result.imported_count == 3
@@ -733,7 +756,7 @@ class TestImportEventsE2E:
caldav = CalDAVClient()
events = caldav.get_events(
user,
calendar.caldav_path,
caldav_path,
start=datetime(2026, 2, 10, tzinfo=dt_tz.utc),
end=datetime(2026, 2, 12, tzinfo=dt_tz.utc),
)
@@ -744,10 +767,10 @@ class TestImportEventsE2E:
def test_import_all_day_event_e2e(self):
"""Import an all-day event and verify it exists in SabreDAV."""
user = factories.UserFactory(email="import-allday@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(user, calendar, ICS_ALL_DAY_EVENT)
result = import_service.import_events(user, caldav_path, ICS_ALL_DAY_EVENT)
assert result.total_events == 1
assert result.imported_count == 1
@@ -757,7 +780,7 @@ class TestImportEventsE2E:
caldav = CalDAVClient()
events = caldav.get_events(
user,
calendar.caldav_path,
caldav_path,
start=datetime(2026, 2, 14, tzinfo=dt_tz.utc),
end=datetime(2026, 2, 17, tzinfo=dt_tz.utc),
)
@@ -767,10 +790,10 @@ class TestImportEventsE2E:
def test_import_with_timezone_e2e(self):
"""Import an event with timezone info and verify it in SabreDAV."""
user = factories.UserFactory(email="import-tz@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(user, calendar, ICS_WITH_TIMEZONE)
result = import_service.import_events(user, caldav_path, ICS_WITH_TIMEZONE)
assert result.total_events == 1
assert result.imported_count == 1
@@ -780,7 +803,7 @@ class TestImportEventsE2E:
caldav = CalDAVClient()
events = caldav.get_events(
user,
calendar.caldav_path,
caldav_path,
start=datetime(2026, 2, 10, tzinfo=dt_tz.utc),
end=datetime(2026, 2, 11, tzinfo=dt_tz.utc),
)
@@ -790,7 +813,7 @@ class TestImportEventsE2E:
def test_import_via_api_e2e(self):
"""Import events via the API endpoint hitting real SabreDAV."""
user = factories.UserFactory(email="import-api@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
client = APIClient()
client.force_login(user)
@@ -799,8 +822,8 @@ class TestImportEventsE2E:
"events.ics", ICS_MULTIPLE_EVENTS, content_type="text/calendar"
)
response = client.post(
f"/api/v1.0/calendars/{calendar.id}/import_events/",
{"file": ics_file},
"/api/v1.0/calendars/import-events/",
{"file": ics_file, "caldav_path": caldav_path},
format="multipart",
)
@@ -814,7 +837,7 @@ class TestImportEventsE2E:
caldav = CalDAVClient()
events = caldav.get_events(
user,
calendar.caldav_path,
caldav_path,
start=datetime(2026, 2, 10, tzinfo=dt_tz.utc),
end=datetime(2026, 2, 12, tzinfo=dt_tz.utc),
)
@@ -828,11 +851,11 @@ class TestImportEventsE2E:
plugin used the wrong callback signature for that event.
"""
user = factories.UserFactory(email="import-attendee@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
# Import event with attendees
import_service = ICSImportService()
result = import_service.import_events(user, calendar, ICS_WITH_ATTENDEES)
result = import_service.import_events(user, caldav_path, ICS_WITH_ATTENDEES)
assert result.total_events == 1
assert result.imported_count == 1
@@ -842,7 +865,7 @@ class TestImportEventsE2E:
caldav = CalDAVClient()
caldav.update_event(
user,
calendar.caldav_path,
caldav_path,
"attendee-event-1",
{"title": "Updated review meeting"},
)
@@ -850,7 +873,7 @@ class TestImportEventsE2E:
# Verify update was applied
events = caldav.get_events(
user,
calendar.caldav_path,
caldav_path,
start=datetime(2026, 2, 10, tzinfo=dt_tz.utc),
end=datetime(2026, 2, 11, tzinfo=dt_tz.utc),
)
@@ -865,11 +888,11 @@ class TestImportEventsE2E:
binds values as PARAM_STR instead of PARAM_LOB.
"""
user = factories.UserFactory(email="import-escapes@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(
user, calendar, ICS_WITH_NEWLINES_IN_DESCRIPTION
user, caldav_path, ICS_WITH_NEWLINES_IN_DESCRIPTION
)
assert result.total_events == 1
@@ -880,7 +903,7 @@ class TestImportEventsE2E:
caldav = CalDAVClient()
events = caldav.get_events(
user,
calendar.caldav_path,
caldav_path,
start=datetime(2026, 2, 10, tzinfo=dt_tz.utc),
end=datetime(2026, 2, 11, tzinfo=dt_tz.utc),
)
@@ -890,17 +913,17 @@ class TestImportEventsE2E:
def test_import_same_file_twice_no_duplicates_e2e(self):
"""Importing the same ICS file twice should not create duplicates."""
user = factories.UserFactory(email="import-dedup@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
# First import
result1 = import_service.import_events(user, calendar, ICS_MULTIPLE_EVENTS)
result1 = import_service.import_events(user, caldav_path, ICS_MULTIPLE_EVENTS)
assert result1.imported_count == 3
assert not result1.errors
# Second import of the same file — all should be duplicates
result2 = import_service.import_events(user, calendar, ICS_MULTIPLE_EVENTS)
result2 = import_service.import_events(user, caldav_path, ICS_MULTIPLE_EVENTS)
assert result2.duplicate_count == 3
assert result2.imported_count == 0
assert result2.skipped_count == 0
@@ -909,7 +932,7 @@ class TestImportEventsE2E:
caldav = CalDAVClient()
events = caldav.get_events(
user,
calendar.caldav_path,
caldav_path,
start=datetime(2026, 2, 10, tzinfo=dt_tz.utc),
end=datetime(2026, 2, 12, tzinfo=dt_tz.utc),
)
@@ -918,21 +941,21 @@ class TestImportEventsE2E:
def test_import_dead_recurring_event_skipped_silently_e2e(self):
"""A recurring event whose EXDATE excludes all instances is skipped, not an error."""
user = factories.UserFactory(email="import-dead-recur@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(user, calendar, ICS_DEAD_RECURRING)
result = import_service.import_events(user, caldav_path, ICS_DEAD_RECURRING)
assert result.total_events == 1
assert result.imported_count == 0
assert result.skipped_count == 1
assert not result.errors
def _get_raw_event(self, user, calendar, uid):
def _get_raw_event(self, user, caldav_path, uid):
"""Fetch the raw ICS data of a single event from SabreDAV by UID."""
caldav_client = CalDAVClient()
client = caldav_client._get_client(user) # pylint: disable=protected-access
cal_url = f"{caldav_client.base_url}{calendar.caldav_path}"
cal_url = f"{caldav_client.base_url}{caldav_path}"
cal = client.calendar(url=cal_url)
event = cal.event_by_uid(uid)
return event.data
@@ -940,11 +963,11 @@ class TestImportEventsE2E:
def test_import_strips_binary_attachments_e2e(self):
"""Binary attachments should be stripped during import."""
user = factories.UserFactory(email="import-strip-attach@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(
user, calendar, ICS_WITH_BINARY_ATTACHMENT
user, caldav_path, ICS_WITH_BINARY_ATTACHMENT
)
assert result.total_events == 1
@@ -952,7 +975,7 @@ class TestImportEventsE2E:
assert not result.errors
# Verify event exists and binary attachment was stripped
raw = self._get_raw_event(user, calendar, "attach-binary-1")
raw = self._get_raw_event(user, caldav_path, "attach-binary-1")
assert "Event with inline attachment" in raw
assert "iVBORw0KGgo" not in raw
assert "ATTACH" not in raw
@@ -960,28 +983,30 @@ class TestImportEventsE2E:
def test_import_keeps_url_attachments_e2e(self):
"""URL-based attachments should NOT be stripped during import."""
user = factories.UserFactory(email="import-keep-url-attach@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(user, calendar, ICS_WITH_URL_ATTACHMENT)
result = import_service.import_events(
user, caldav_path, ICS_WITH_URL_ATTACHMENT
)
assert result.total_events == 1
assert result.imported_count == 1
assert not result.errors
# Verify URL attachment is preserved in raw ICS
raw = self._get_raw_event(user, calendar, "attach-url-1")
raw = self._get_raw_event(user, caldav_path, "attach-url-1")
assert "https://example.com/doc.pdf" in raw
assert "ATTACH" in raw
def test_import_truncates_large_description_e2e(self):
"""Descriptions exceeding IMPORT_MAX_DESCRIPTION_BYTES should be truncated."""
user = factories.UserFactory(email="import-trunc-desc@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(
user, calendar, ICS_WITH_LARGE_DESCRIPTION
user, caldav_path, ICS_WITH_LARGE_DESCRIPTION
)
assert result.total_events == 1
@@ -989,7 +1014,7 @@ class TestImportEventsE2E:
assert not result.errors
# Verify description was truncated (default 100KB limit, original 200KB)
raw = self._get_raw_event(user, calendar, "large-desc-1")
raw = self._get_raw_event(user, caldav_path, "large-desc-1")
assert "Event with huge description" in raw
# Raw ICS should be much smaller than the 200KB original
assert len(raw) < 150000
@@ -1005,15 +1030,15 @@ class TestCalendarSanitizerE2E:
"""E2E tests for CalendarSanitizerPlugin on normal CalDAV PUT operations."""
def _create_calendar(self, user):
"""Create a real calendar in both Django and SabreDAV."""
"""Create a real calendar in SabreDAV. Returns the caldav_path."""
service = CalendarService()
return service.create_calendar(user, name="Sanitizer Test", color="#3174ad")
def _get_raw_event(self, user, calendar, uid):
def _get_raw_event(self, user, caldav_path, uid):
"""Fetch the raw ICS data of a single event from SabreDAV by UID."""
caldav_client = CalDAVClient()
client = caldav_client._get_client(user) # pylint: disable=protected-access
cal_url = f"{caldav_client.base_url}{calendar.caldav_path}"
cal_url = f"{caldav_client.base_url}{caldav_path}"
cal = client.calendar(url=cal_url)
event = cal.event_by_uid(uid)
return event.data
@@ -1021,14 +1046,12 @@ class TestCalendarSanitizerE2E:
def test_caldav_put_strips_binary_attachment_e2e(self):
"""A normal CalDAV PUT with binary attachment should be sanitized."""
user = factories.UserFactory(email="sanitizer-put-attach@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
caldav = CalDAVClient()
caldav.create_event_raw(
user, calendar.caldav_path, ICS_WITH_BINARY_ATTACHMENT.decode()
)
caldav.create_event_raw(user, caldav_path, ICS_WITH_BINARY_ATTACHMENT.decode())
raw = self._get_raw_event(user, calendar, "attach-binary-1")
raw = self._get_raw_event(user, caldav_path, "attach-binary-1")
assert "Event with inline attachment" in raw
assert "iVBORw0KGgo" not in raw
assert "ATTACH" not in raw
@@ -1036,28 +1059,24 @@ class TestCalendarSanitizerE2E:
def test_caldav_put_keeps_url_attachment_e2e(self):
"""A normal CalDAV PUT with URL attachment should preserve it."""
user = factories.UserFactory(email="sanitizer-put-url@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
caldav = CalDAVClient()
caldav.create_event_raw(
user, calendar.caldav_path, ICS_WITH_URL_ATTACHMENT.decode()
)
caldav.create_event_raw(user, caldav_path, ICS_WITH_URL_ATTACHMENT.decode())
raw = self._get_raw_event(user, calendar, "attach-url-1")
raw = self._get_raw_event(user, caldav_path, "attach-url-1")
assert "https://example.com/doc.pdf" in raw
assert "ATTACH" in raw
def test_caldav_put_truncates_large_description_e2e(self):
"""A normal CalDAV PUT with oversized description should be truncated."""
user = factories.UserFactory(email="sanitizer-put-desc@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
caldav = CalDAVClient()
caldav.create_event_raw(
user, calendar.caldav_path, ICS_WITH_LARGE_DESCRIPTION.decode()
)
caldav.create_event_raw(user, caldav_path, ICS_WITH_LARGE_DESCRIPTION.decode())
raw = self._get_raw_event(user, calendar, "large-desc-1")
raw = self._get_raw_event(user, caldav_path, "large-desc-1")
assert "Event with huge description" in raw
assert len(raw) < 150000
assert "..." in raw
@@ -1065,23 +1084,21 @@ class TestCalendarSanitizerE2E:
def test_caldav_put_rejects_oversized_event_e2e(self):
"""A CalDAV PUT exceeding max-resource-size should be rejected (HTTP 507)."""
user = factories.UserFactory(email="sanitizer-put-oversize@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
caldav = CalDAVClient()
with pytest.raises(Exception) as exc_info:
caldav.create_event_raw(
user, calendar.caldav_path, ICS_OVERSIZED_EVENT.decode()
)
caldav.create_event_raw(user, caldav_path, ICS_OVERSIZED_EVENT.decode())
# SabreDAV returns 507 Insufficient Storage
assert "507" in str(exc_info.value) or "Insufficient" in str(exc_info.value)
def test_import_rejects_oversized_event_e2e(self):
"""Import of an event exceeding max-resource-size should skip it."""
user = factories.UserFactory(email="sanitizer-import-oversize@example.com")
calendar = self._create_calendar(user)
caldav_path = self._create_calendar(user)
import_service = ICSImportService()
result = import_service.import_events(user, calendar, ICS_OVERSIZED_EVENT)
result = import_service.import_events(user, caldav_path, ICS_OVERSIZED_EVENT)
assert result.total_events == 1
assert result.imported_count == 0