caldav_sync: fixed a bug in events created from appointments, added test module test_caldav_sync_appointments
This commit is contained in:
parent
800214b19c
commit
ece737826c
5 changed files with 223 additions and 3 deletions
|
|
@ -223,7 +223,9 @@ class CalendarEvent(models.Model):
|
|||
{"caldav_uid": caldav_uid}
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to sync event to CalDAV server: {e}")
|
||||
_logger.error(
|
||||
f"Failed to sync event to CalDAV server: {e}", exc_info=True
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
res = super(CalendarEvent, self.with_context(caldav_no_sync=True)).write(vals)
|
||||
|
|
@ -347,7 +349,6 @@ class CalendarEvent(models.Model):
|
|||
calendar = client.calendar(url=user.caldav_calendar_url)
|
||||
try:
|
||||
caldav_event = calendar.event_by_uid(self.caldav_uid)
|
||||
assert isinstance(caldav_event, caldav.Event)
|
||||
if not delete_all and self.recurrence_id and not self.is_base_event:
|
||||
index = self._get_subcomponent_index_for_recurrence(
|
||||
caldav_event
|
||||
|
|
@ -488,7 +489,12 @@ class CalendarEvent(models.Model):
|
|||
|
||||
def _add_event_dates(self, event_data: Dict) -> None:
|
||||
"""Add pertinent dates to event data, based on self."""
|
||||
tz = self.event_tz or self.env.user.tz
|
||||
# Determine timezone: prefer event_tz, then user tz, finally UTC
|
||||
# Note: All datetimes in Odoo are stored in UTC, so defaulting to UTC is correct.
|
||||
# UTC times are sent in from the appointments app when installed, without
|
||||
# timezone information. This was breaking the sync process due to a call to
|
||||
# upper() on boolean value False.
|
||||
tz = self.event_tz or self.env.user.tz or "UTC"
|
||||
event_tz = timezone(tz)
|
||||
event_data["last-modified"] = vDatetime(
|
||||
utc.localize(self.write_date).astimezone(event_tz)
|
||||
|
|
|
|||
2
test_caldav_sync_appointments/__init__.py
Normal file
2
test_caldav_sync_appointments/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Test module for CalDAV sync appointments integration
|
||||
28
test_caldav_sync_appointments/__manifest__.py
Normal file
28
test_caldav_sync_appointments/__manifest__.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Test CalDAV Sync Appointments Integration',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Hidden/Tests',
|
||||
'summary': 'Test module for CalDAV sync integration with appointments',
|
||||
'description': """
|
||||
Test CalDAV Sync Appointments Integration
|
||||
=========================================
|
||||
|
||||
This module contains tests for the integration between CalDAV sync and the
|
||||
Appointments app. It verifies that appointment events are properly handled
|
||||
during CalDAV synchronization operations.
|
||||
|
||||
This is a test-only module and should not be installed in production.
|
||||
""",
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'depends': [
|
||||
'caldav_sync',
|
||||
'appointment',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
# Mark as test module
|
||||
'post_init_hook': None,
|
||||
}
|
||||
1
test_caldav_sync_appointments/tests/__init__.py
Normal file
1
test_caldav_sync_appointments/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_appointment_caldav_integration
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
"""Test CalDAV sync integration with appointment events."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import contextmanager
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo import Command
|
||||
from odoo.addons.caldav_sync.tests.common import CaldavTestCommon
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _patch_caldav_for_appointments(user):
|
||||
"""Patch CalDAV operations for appointment testing, capturing real iCalendar events."""
|
||||
with mute_logger("odoo.addons.caldav_sync.models.res_users"), patch(
|
||||
"caldav.DAVClient"
|
||||
) as MockDAVClient:
|
||||
mock_client = MockDAVClient.return_value
|
||||
mock_calendar = MagicMock()
|
||||
mock_client.calendar.return_value = mock_calendar
|
||||
|
||||
# Storage for created CalDAV events and global delete tracking
|
||||
created_events = {} # uid -> caldav_event
|
||||
global_delete_calls = [] # Track all delete calls
|
||||
|
||||
def save_event_side_effect(**ical_data):
|
||||
"""Let the real CalDAV event creation happen, then capture and store it."""
|
||||
# Create a mock CalDAV event that we can track delete() calls on
|
||||
mock_caldav_event = MagicMock()
|
||||
|
||||
# Set up the vobject_instance structure that CalDAV sync expects
|
||||
uid = ical_data.get("uid", f"test-uid-{len(created_events)}")
|
||||
mock_caldav_event.vobject_instance.vevent.uid.value = uid
|
||||
|
||||
# Ensure delete method is properly trackable and records calls globally
|
||||
def delete_side_effect():
|
||||
global_delete_calls.append(uid)
|
||||
|
||||
mock_caldav_event.delete = MagicMock(side_effect=delete_side_effect)
|
||||
|
||||
# Set up icalendar_component structure using the real iCalendar data
|
||||
mock_ical_component = MagicMock()
|
||||
|
||||
def ical_component_get(key):
|
||||
if key == "dtstart":
|
||||
mock_dtstart = MagicMock()
|
||||
dtstart_value = ical_data.get("dtstart")
|
||||
|
||||
# Handle vDatetime objects from iCalendar library
|
||||
if hasattr(dtstart_value, "dt"):
|
||||
# It's already a vDatetime, extract the actual datetime
|
||||
actual_dt = dtstart_value.dt
|
||||
else:
|
||||
# It's a regular datetime
|
||||
actual_dt = dtstart_value
|
||||
|
||||
# Create a mock that has tzinfo like a regular datetime
|
||||
mock_dt = MagicMock()
|
||||
mock_dt.tzinfo = getattr(actual_dt, "tzinfo", None)
|
||||
mock_dt.__eq__ = lambda self, other: actual_dt == other
|
||||
|
||||
mock_dtstart.dt = mock_dt
|
||||
return mock_dtstart
|
||||
return MagicMock()
|
||||
|
||||
mock_ical_component.get.side_effect = ical_component_get
|
||||
mock_caldav_event.icalendar_component = mock_ical_component
|
||||
|
||||
# Store the event by UID for later retrieval
|
||||
created_events[uid] = mock_caldav_event
|
||||
|
||||
return mock_caldav_event
|
||||
|
||||
def event_by_uid_side_effect(uid):
|
||||
"""Return the previously created CalDAV event by UID."""
|
||||
if uid in created_events:
|
||||
# Always return the same mock object for the same UID
|
||||
return created_events[uid]
|
||||
# Raise NotFoundError if event doesn't exist (like real CalDAV)
|
||||
from caldav.lib.error import NotFoundError
|
||||
|
||||
raise NotFoundError("Event not found")
|
||||
|
||||
# Set up the mock methods
|
||||
mock_calendar.events.return_value = []
|
||||
mock_calendar.save_event.side_effect = save_event_side_effect
|
||||
mock_calendar.event_by_uid.side_effect = event_by_uid_side_effect
|
||||
|
||||
# Enable CalDAV for the user
|
||||
user.write({"is_caldav_enabled": True})
|
||||
|
||||
# Return both the mock calendar and the global delete tracking
|
||||
mock_calendar.global_delete_calls = global_delete_calls
|
||||
yield mock_calendar
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestAppointmentCalDAVIntegration(TransactionCase, CaldavTestCommon):
|
||||
"""Test CalDAV sync behavior with appointment events."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Create test user with CalDAV enabled using the common method
|
||||
cls.test_user = cls._generate_user(
|
||||
"test_caldav_user",
|
||||
caldav_username="testuser",
|
||||
caldav_password="testpass",
|
||||
caldav_url="https://test.caldav.server/calendar/testuser",
|
||||
)
|
||||
|
||||
# Create test partner
|
||||
cls.test_partner = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Partner",
|
||||
"email": "partner@example.com",
|
||||
}
|
||||
)
|
||||
|
||||
# Create appointment type
|
||||
cls.appointment_type = cls.env["appointment.type"].create(
|
||||
{
|
||||
"category": "custom",
|
||||
"appointment_duration": 1.0,
|
||||
"staff_user_ids": [Command.link(cls.test_user.id)],
|
||||
}
|
||||
)
|
||||
|
||||
def test_appointment_booking_syncs_to_caldav(self):
|
||||
"""Test that appointments created through the booking interface sync to CalDAV.
|
||||
|
||||
BUG: When a public user books an appointment through the booking interface,
|
||||
the event is created in Odoo but NOT synced to the CalDAV server. This causes
|
||||
the event to be deleted on the next polling round since CalDAV sync thinks it
|
||||
was deleted from the server.
|
||||
"""
|
||||
|
||||
with _patch_caldav_for_appointments(self.test_user) as mock_calendar:
|
||||
# Simulate the appointment booking flow - appointments are created with
|
||||
# the staff user but initiated by a public/portal user
|
||||
# The key is that we need to simulate how the appointment controller creates events
|
||||
|
||||
# Create appointment event as it would be created during booking
|
||||
appointment_event = (
|
||||
self.env["calendar.event"]
|
||||
.with_user(self.test_user)
|
||||
.create(
|
||||
{
|
||||
"name": f"{self.appointment_type.name} with {self.test_user.name}",
|
||||
"start": datetime.now() + timedelta(days=1),
|
||||
"stop": datetime.now() + timedelta(days=1, hours=1),
|
||||
"user_id": self.test_user.id,
|
||||
"partner_ids": [(4, self.test_partner.id)],
|
||||
"appointment_type_id": self.appointment_type.id,
|
||||
"appointment_status": "booked",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Debug output
|
||||
print(f"\nDEBUG Appointment Booking:")
|
||||
print(f" save_event called: {mock_calendar.save_event.called}")
|
||||
print(f" save_event call_count: {mock_calendar.save_event.call_count}")
|
||||
print(f" caldav_uid: {appointment_event.caldav_uid}")
|
||||
print(f" user_id: {appointment_event.user_id.name}")
|
||||
print(f" is_caldav_enabled: {appointment_event._is_caldav_enabled()}")
|
||||
|
||||
# THE BUG: Appointment events should be synced to CalDAV on creation
|
||||
# This will FAIL if the bug exists
|
||||
self.assertTrue(
|
||||
mock_calendar.save_event.called,
|
||||
"BUG: Appointment event was NOT synced to CalDAV server on creation! "
|
||||
"This will cause it to be deleted on next polling round.",
|
||||
)
|
||||
self.assertTrue(
|
||||
appointment_event.caldav_uid,
|
||||
"BUG: Appointment event has no caldav_uid! Event will be deleted on next sync.",
|
||||
)
|
||||
Loading…
Reference in a new issue