From 9243fad2e6e204f3f189c78e0a17a8a71061de8e Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Mon, 7 Oct 2024 17:13:52 -0400 Subject: [PATCH] bunch of new test cases and code mods --- caldav_sync/models/calendar_event.py | 43 ++++--- caldav_sync/tests/common.py | 5 +- caldav_sync/tests/data/test_recurring.ics | 38 ++++++ caldav_sync/tests/test_calendar.py | 145 +++++++++++++++++++++- 4 files changed, 207 insertions(+), 24 deletions(-) create mode 100644 caldav_sync/tests/data/test_recurring.ics diff --git a/caldav_sync/models/calendar_event.py b/caldav_sync/models/calendar_event.py index 7f765d1..63e22a4 100644 --- a/caldav_sync/models/calendar_event.py +++ b/caldav_sync/models/calendar_event.py @@ -59,9 +59,9 @@ class CalendarEvent(models.Model): @api.depends("user_id", "partner_ids", "partner_ids.user_id") def _compute_caldav_users(self): for rec in self: - rec.caldav_user_ids = ( - rec.user_id | rec.partner_ids.mapped("user_id") - ).filtered(lambda user: user.is_caldav_enabled) + rec.caldav_user_ids = (rec.user_id | rec.partner_ids.user_ids).filtered( + "is_caldav_enabled" + ) @api.model_create_multi def create(self, vals_list): @@ -249,7 +249,8 @@ class CalendarEvent(models.Model): """Match the fields from calendar.event (recurring fields) to the fields specified in RRULE at https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html""" - rrule = component.get("rrule") + rrule = [item[1] for item in component.property_items() if item[0] == "RRULE"] + rrule = rrule[0] if rrule else None if not rrule: if not self.recurrency: # No change, this was already not a recurring event @@ -268,7 +269,7 @@ class CalendarEvent(models.Model): return {"recurrence_update": "all_events", "recurrency": False} rrule_str = rrule.to_ical().decode("utf-8") sequence = component.get("sequence") - if sequence and sequence != 0: + if sequence and sequence != 1: # This is not the base event so we can't change recurrence properties return {} @@ -315,15 +316,19 @@ class CalendarEvent(models.Model): partner_ids = self._get_attendee_partners(component, current_user_email) existing_instance = self._get_existing_instance(uid, recurrence_id) + outdated = False last_modified = component.decoded("last-modified") if existing_instance and last_modified: last_modified = last_modified.astimezone(utc).replace(tzinfo=None) if last_modified < existing_instance.write_date: - _logger.info( - f"Last modified date {last_modified} is before most recent " - f"write date {existing_instance.write_date}. Skipping." - ) - continue + # _logger.info( + # f"Last modified date {last_modified} is before most recent " + # f"write date {existing_instance.write_date}. Skipping." + # ) + outdated = True + owned = ( + existing_instance and existing_instance.partner_id == user.partner_id + ) values, recurrency_vals = ( self._get_vals_recurrency_vals_from_ical_component( partner_ids, component, user @@ -333,6 +338,14 @@ class CalendarEvent(models.Model): if not existing_instance: _logger.info(f"Creating with vals: {values}") self.with_context(caldav_no_sync=True).create(values) + elif outdated or not owned: + _logger.info( + f"Event {existing_instance.caldav_uid} " + f"{'outdated ' if outdated else ''}" + f"{'not owned by user ' + user.name if not owned else ''}." + f" Skipping." + ) + pass # Do nothing, it's not this user's event to modify or it's outdated else: # Don't update partner_ids if no change if partner_ids != existing_instance.partner_ids: @@ -473,13 +486,3 @@ class CalendarEvent(models.Model): "tentative": "TENTATIVE", } return mapping.get(state, "NEEDS-ACTION") - - @staticmethod - def _map_ical_status(ical_status): - mapping = { - "NEEDS-ACTION": "needsAction", - "ACCEPTED": "accepted", - "DECLINED": "declined", - "TENTATIVE": "tentative", - } - return mapping.get(ical_status, "needsAction") diff --git a/caldav_sync/tests/common.py b/caldav_sync/tests/common.py index 71ff28f..6c9957e 100644 --- a/caldav_sync/tests/common.py +++ b/caldav_sync/tests/common.py @@ -6,12 +6,15 @@ class CaldavTestCommon: def _generate_user( cls, name, caldav_username=None, caldav_password=None, caldav_url=None ): + groups_ids = cls.env.ref("base.group_user") | cls.env.ref( + "base.group_partner_manager" + ) vals = { "name": name, "login": name, "password": name, "email": name + "@example.com", - "groups_id": [Command.set(cls.env.ref("base.group_user").ids)], + "groups_id": [Command.set(groups_ids.ids)], } if caldav_username: vals.update(caldav_username=caldav_username) diff --git a/caldav_sync/tests/data/test_recurring.ics b/caldav_sync/tests/data/test_recurring.ics new file mode 100644 index 0000000..fa5400c --- /dev/null +++ b/caldav_sync/tests/data/test_recurring.ics @@ -0,0 +1,38 @@ +BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +X-WR-CALNAME:test recurring +METHOD:PUBLISH +PRODID:-//Apple Inc.//macOS 14.5//EN +BEGIN:VTIMEZONE +TZID:America/Toronto +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +DTSTART:20070311T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +TZNAME:EDT +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +DTSTART:20071104T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +TZNAME:EST +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +TRANSP:OPAQUE +DTEND;TZID=America/Toronto:20241008T040000 +X-APPLE-CREATOR-TEAM-IDENTITY:0000000000 +UID:F777C16F-2F14-4326-9681-69C6043DB8C0 +DTSTAMP:20241007T195255Z +X-APPLE-CREATOR-IDENTITY:com.apple.calendar +SEQUENCE:1 +SUMMARY:test recurring +DTSTART;TZID=America/Toronto:20241008T030000 +LAST-MODIFIED:20241007T195252Z +CREATED:20241007T195223Z +RRULE:FREQ=WEEKLY;COUNT=10 +END:VEVENT +END:VCALENDAR diff --git a/caldav_sync/tests/test_calendar.py b/caldav_sync/tests/test_calendar.py index 1f3bf90..3bc2706 100644 --- a/caldav_sync/tests/test_calendar.py +++ b/caldav_sync/tests/test_calendar.py @@ -8,6 +8,16 @@ from .common import CaldavTestCommon from contextlib import contextmanager from datetime import datetime, UTC, timedelta +WEEKDAY_MAP = { + 0: "SUN", + 1: "MON", + 2: "TUE", + 3: "WED", + 4: "THU", + 5: "FRI", + 6: "SAT", +} + def _get_ics_path(filename): return Path(__file__).parent / "data" / filename @@ -23,10 +33,13 @@ def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None): mock_client = MockDAVClient.return_value mock_calendar = MockCalendar.return_value mock_client.calendar = mock_calendar + mock_calendars = {} def calendar_side_effect(url): + if url not in mock_calendars: + mock_calendars[url] = MockCalendar() if url == user.caldav_calendar_url: - return mock_calendar + return mock_calendars[url] raise Exception("Calendar does not exist.") mock_calendar.side_effect = calendar_side_effect @@ -109,6 +122,16 @@ class TestCalendarEvent(TransactionCase, CaldavTestCommon): event = self.env["calendar.event"].search([("user_id", "=", user.id)]) self.assertFalse(event) + def test_recurring_from_server_create(self): + user = self.user_1 + ics_path = _get_ics_path("test_recurring.ics") + with _patch_caldav_with_events_from_ics(ics_path, user): + self.env["calendar.event"].poll_caldav_server() + events = self.env["calendar.event"].search( + [("partner_id", "=", user.partner_id.id)] + ) + self.assertEqual(len(events), 10) + def test_multiple_attendees_event_from_server_create(self): user = self.user_1 ics_path = _get_ics_path("test_multi_attendee.ics") @@ -154,9 +177,125 @@ class TestCalendarEvent(TransactionCase, CaldavTestCommon): ics_path = _get_ics_path("test_multi_user.ics") with _patch_caldav_with_events_from_ics(ics_path, user1): self.env["calendar.event"].poll_caldav_server() + with _patch_caldav_with_events_from_ics(ics_path, user2): + self.env["calendar.event"].poll_caldav_server() + with _patch_caldav_with_events_from_ics(ics_path, user3): + self.env["calendar.event"].poll_caldav_server() + event = self.env["calendar.event"].search( + [("caldav_uid", "=", "2495546B-5C9A-4632-AAD3-A179EF83CF20")] + ) + self.assertEqual(len(event), 1) + # Make sure the event wasn't duplicated all over the place + other_user_events = self.env["calendar.event"].search( + [("user_id", "in", [user2.id, user3.id])] + ) + self.assertFalse(other_user_events) + self.assertIn(user2.partner_id, event.partner_ids) + self.assertIn(user3.partner_id, event.partner_ids) def test_multiple_user_attendees_event_from_server_update(self): - pass + """Test event has (as in above test): + Organizer: user1 (test1@example.com) + Attendees: user2 and user3 (test2@example.com, test3@example.com) + """ + user1 = self.user_1 + user2 = self.user_2 + user3 = self.user_3 + ics_path = _get_ics_path("test_multi_user.ics") + with _patch_caldav_with_events_from_ics(ics_path, user1): + self.env["calendar.event"].poll_caldav_server() + with _patch_caldav_with_events_from_ics(ics_path, user2): + self.env["calendar.event"].poll_caldav_server() + with _patch_caldav_with_events_from_ics(ics_path, user3): + self.env["calendar.event"].poll_caldav_server() + # Now update it to remove one attendee + # Shuffle the user polling order just to test more robustly + ics_path = _get_ics_path("test_multi_user_update.ics") + with _patch_caldav_with_events_from_ics( + ics_path, user2, last_modified=datetime.now(UTC) + ): + self.env["calendar.event"].poll_caldav_server() + with _patch_caldav_with_events_from_ics( + ics_path, user3, last_modified=datetime.now(UTC) + ): + self.env["calendar.event"].poll_caldav_server() + with _patch_caldav_with_events_from_ics( + ics_path, user1, last_modified=datetime.now(UTC) + ): + self.env["calendar.event"].poll_caldav_server() + event = self.env["calendar.event"].search( + [("caldav_uid", "=", "2495546B-5C9A-4632-AAD3-A179EF83CF20")] + ) + self.assertIn(user3.partner_id, event.partner_ids) + self.assertNotIn(user2.partner_id, event.partner_ids) + self.assertEqual(len(event.attendee_ids), 2) + + def _create_multi_user_test_event(self): + return ( + self.env["calendar.event"] + .with_user(self.user_1) + .create( + { + "name": "Test event", + "partner_ids": [ + Command.set( + [ + self.user_2.partner_id.id, + self.user_3.partner_id.id, + self.user_1.partner_id.id, + ] + ), + Command.create( + { + "name": "Test partner", + "email": "testpartner@example.com", + } + ), + ], + "start": datetime.now() + timedelta(days=2), + "stop": datetime.now() + timedelta(days=2, hours=1), + } + ) + ) def test_multiple_user_attendees_event_to_server_create(self): - pass + with self._patch_all_3_users_davclients() as (_, mock_calendar): + self._create_multi_user_test_event() + self.assertEqual(mock_calendar.add_event.call_count, 3) + + def test_event_to_server_delete(self): + with self._patch_all_3_users_davclients() as (_, mock_calendar): + self._create_multi_user_test_event().unlink() + self.assertEqual( + mock_calendar.event_by_uid.return_value.delete.call_count, 3 + ) + + def test_event_to_server_update(self): + with self._patch_all_3_users_davclients() as (_, mock_calendar): + self._create_multi_user_test_event().write( + {"start": datetime.now() + timedelta(days=14)} + ) + self.assertEqual(mock_calendar.save_event.call_count, 3) + + def test_recurrent_event_to_server(self): + with self._patch_all_3_users_davclients() as (_, mock_calendar): + self._create_multi_user_test_event().write( + { + "recurrency": True, + } + ) + args = mock_calendar.save_event.call_args + self.assertEqual(mock_calendar.save_event.call_count, 3) + + @contextmanager + def _patch_all_3_users_davclients(self): + with patch("caldav.DAVClient") as MockDAVClient: + (self.user_1 | self.user_2 | self.user_3)._compute_is_caldav_enabled() + + mock_client = MockDAVClient.return_value + mock_calendar = MagicMock() + mock_event_by_uid = MagicMock() + mock_client.calendar.return_value = mock_calendar + mock_calendar.events.return_value = [] + mock_calendar.event_by_uid.return_value = mock_event_by_uid + yield mock_client, mock_calendar