mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-23 15:32:08 +02:00
Initial commit: Security packages
This commit is contained in:
commit
bb469e4763
1399 changed files with 278378 additions and 0 deletions
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_microsoft_event
|
||||
from . import test_microsoft_service
|
||||
from . import test_create_events
|
||||
from . import test_update_events
|
||||
from . import test_delete_events
|
||||
from . import test_answer_events
|
||||
|
|
@ -0,0 +1,549 @@
|
|||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from markupsafe import Markup
|
||||
from unittest.mock import patch, MagicMock
|
||||
from contextlib import contextmanager
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import fields
|
||||
|
||||
from odoo.tests.common import HttpCase
|
||||
|
||||
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftSync
|
||||
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
|
||||
|
||||
def mock_get_token(user):
|
||||
return f"TOKEN_FOR_USER_{user.id}"
|
||||
|
||||
def _modified_date_in_the_future(event):
|
||||
"""
|
||||
Add some seconds to the event write date to be sure to have a last modified date
|
||||
in the future
|
||||
"""
|
||||
return (event.write_date + timedelta(seconds=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
def patch_api(func):
|
||||
@patch.object(MicrosoftSync, '_microsoft_insert', MagicMock())
|
||||
@patch.object(MicrosoftSync, '_microsoft_delete', MagicMock())
|
||||
@patch.object(MicrosoftSync, '_microsoft_patch', MagicMock())
|
||||
def patched(self, *args, **kwargs):
|
||||
return func(self, *args, **kwargs)
|
||||
return patched
|
||||
|
||||
# By inheriting from TransactionCase, postcommit hooks (so methods tagged with `@after_commit` in MicrosoftSync),
|
||||
# are not called because no commit is done.
|
||||
# To be able to manually call these postcommit hooks, we need to inherit from HttpCase.
|
||||
# Note: as postcommit hooks are called separately, do not forget to invalidate cache for records read during the test.
|
||||
class TestCommon(HttpCase):
|
||||
|
||||
@patch_api
|
||||
def setUp(self):
|
||||
super(TestCommon, self).setUp()
|
||||
|
||||
# prepare users
|
||||
self.organizer_user = self.env["res.users"].search([("name", "=", "Mike Organizer")])
|
||||
if not self.organizer_user:
|
||||
partner = self.env['res.partner'].create({'name': 'Mike Organizer', 'email': 'mike@organizer.com'})
|
||||
self.organizer_user = self.env['res.users'].create({
|
||||
'name': 'Mike Organizer',
|
||||
'login': 'mike@organizer.com',
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
|
||||
self.attendee_user = self.env["res.users"].search([("name", "=", "John Attendee")])
|
||||
if not self.attendee_user:
|
||||
partner = self.env['res.partner'].create({'name': 'John Attendee', 'email': 'john@attendee.com'})
|
||||
self.attendee_user = self.env['res.users'].create({
|
||||
'name': 'John Attendee',
|
||||
'login': 'john@attendee.com',
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
|
||||
# Add token validity with one hour of time window for properly checking the sync status.
|
||||
for user in [self.organizer_user, self.attendee_user]:
|
||||
user.microsoft_calendar_token_validity = fields.Datetime.now() + timedelta(hours=1)
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# To create Odoo events
|
||||
# -----------------------------------------------------------------------------------------
|
||||
self.start_date = datetime(2021, 9, 22, 10, 0, 0, 0)
|
||||
self.end_date = datetime(2021, 9, 22, 11, 0, 0, 0)
|
||||
self.recurrent_event_interval = 2
|
||||
self.recurrent_events_count = 7
|
||||
self.recurrence_end_date = self.end_date + timedelta(
|
||||
days=self.recurrent_event_interval * self.recurrent_events_count
|
||||
)
|
||||
|
||||
# simple event values to create a Odoo event
|
||||
self.simple_event_values = {
|
||||
"name": "simple_event",
|
||||
"description": "my simple event",
|
||||
"active": True,
|
||||
"start": self.start_date,
|
||||
"stop": self.end_date,
|
||||
"partner_ids": [(4, self.organizer_user.partner_id.id), (4, self.attendee_user.partner_id.id)],
|
||||
}
|
||||
self.recurrent_event_values = {
|
||||
'name': 'recurring_event',
|
||||
'description': 'a recurring event',
|
||||
"partner_ids": [(4, self.attendee_user.partner_id.id)],
|
||||
'recurrency': True,
|
||||
'follow_recurrence': True,
|
||||
'start': self.start_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'stop': self.end_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'event_tz': 'Europe/London',
|
||||
'recurrence_update': 'self_only',
|
||||
'rrule_type': 'daily',
|
||||
'interval': self.recurrent_event_interval,
|
||||
'count': self.recurrent_events_count,
|
||||
'end_type': 'count',
|
||||
'duration': 1,
|
||||
'byday': '-1',
|
||||
'day': 22,
|
||||
'wed': True,
|
||||
'weekday': 'WED'
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Expected values for Odoo events converted to Outlook events (to be posted through API)
|
||||
# -----------------------------------------------------------------------------------------
|
||||
|
||||
# simple event values converted in the Outlook format to be posted through the API
|
||||
self.simple_event_ms_values = {
|
||||
"subject": self.simple_event_values["name"],
|
||||
"body": {
|
||||
'content': self.simple_event_values["description"],
|
||||
'contentType': "text",
|
||||
},
|
||||
"start": {
|
||||
'dateTime': pytz.utc.localize(self.simple_event_values["start"]).isoformat(),
|
||||
'timeZone': 'Europe/London'
|
||||
},
|
||||
"end": {
|
||||
'dateTime': pytz.utc.localize(self.simple_event_values["stop"]).isoformat(),
|
||||
'timeZone': 'Europe/London'
|
||||
},
|
||||
"isAllDay": False,
|
||||
"organizer": {
|
||||
'emailAddress': {
|
||||
'address': self.organizer_user.email,
|
||||
'name': self.organizer_user.display_name,
|
||||
}
|
||||
},
|
||||
"isOrganizer": True,
|
||||
"sensitivity": "normal",
|
||||
"showAs": "busy",
|
||||
"attendees": [
|
||||
{
|
||||
'emailAddress': {
|
||||
'address': self.attendee_user.email,
|
||||
'name': self.attendee_user.display_name
|
||||
},
|
||||
'status': {'response': "notresponded"}
|
||||
}
|
||||
],
|
||||
"isReminderOn": False,
|
||||
"location": {'displayName': ''},
|
||||
"reminderMinutesBeforeStart": 0,
|
||||
}
|
||||
|
||||
self.recurrent_event_ms_values = {
|
||||
'subject': self.recurrent_event_values["name"],
|
||||
"body": {
|
||||
'content': Markup('<p>%s</p>' % self.recurrent_event_values["description"]),
|
||||
'contentType': "html",
|
||||
},
|
||||
'start': {
|
||||
'dateTime': self.start_date.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
|
||||
'timeZone': 'Europe/London'
|
||||
},
|
||||
'end': {
|
||||
'dateTime': self.end_date.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
|
||||
'timeZone': 'Europe/London'
|
||||
},
|
||||
'isAllDay': False,
|
||||
'isOrganizer': True,
|
||||
'isReminderOn': False,
|
||||
'reminderMinutesBeforeStart': 0,
|
||||
'sensitivity': 'normal',
|
||||
'showAs': 'busy',
|
||||
'type': 'seriesMaster',
|
||||
"attendees": [
|
||||
{
|
||||
'emailAddress': {
|
||||
'address': self.attendee_user.email,
|
||||
'name': self.attendee_user.display_name
|
||||
},
|
||||
'status': {'response': "notresponded"}
|
||||
}
|
||||
],
|
||||
'location': {'displayName': ''},
|
||||
'organizer': {
|
||||
'emailAddress': {
|
||||
'address': self.organizer_user.email,
|
||||
'name': self.organizer_user.display_name,
|
||||
},
|
||||
},
|
||||
'recurrence': {
|
||||
'pattern': {'dayOfMonth': 22, 'interval': self.recurrent_event_interval, 'type': 'daily'},
|
||||
'range': {
|
||||
'numberOfOccurrences': self.recurrent_events_count,
|
||||
'startDate': self.start_date.strftime("%Y-%m-%d"),
|
||||
'type': 'numbered'
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Events coming from Outlook (so from the API)
|
||||
# -----------------------------------------------------------------------------------------
|
||||
|
||||
self.simple_event_from_outlook_organizer = {
|
||||
'type': 'singleInstance',
|
||||
'seriesMasterId': None,
|
||||
'id': '123',
|
||||
'iCalUId': '456',
|
||||
'subject': 'simple_event',
|
||||
'body': {
|
||||
'content': "my simple event",
|
||||
'contentType': "text",
|
||||
},
|
||||
'start': {'dateTime': self.start_date.strftime("%Y-%m-%dT%H:%M:%S.0000000"), 'timeZone': 'UTC'},
|
||||
'end': {'dateTime': self.end_date.strftime("%Y-%m-%dT%H:%M:%S.0000000"), 'timeZone': 'UTC'},
|
||||
'attendees': [{
|
||||
'type': 'required',
|
||||
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
|
||||
'emailAddress': {'name': self.attendee_user.display_name, 'address': self.attendee_user.email}
|
||||
}],
|
||||
'isAllDay': False,
|
||||
'isCancelled': False,
|
||||
'sensitivity': 'normal',
|
||||
'showAs': 'busy',
|
||||
'isOnlineMeeting': False,
|
||||
'onlineMeetingUrl': None,
|
||||
'isOrganizer': True,
|
||||
'isReminderOn': True,
|
||||
'location': {'displayName': ''},
|
||||
'organizer': {
|
||||
'emailAddress': {'address': self.organizer_user.email, 'name': self.organizer_user.display_name},
|
||||
},
|
||||
'reminderMinutesBeforeStart': 15,
|
||||
'responseRequested': True,
|
||||
'responseStatus': {
|
||||
'response': 'organizer',
|
||||
'time': '0001-01-01T00:00:00Z',
|
||||
},
|
||||
}
|
||||
|
||||
self.simple_event_from_outlook_attendee = self.simple_event_from_outlook_organizer
|
||||
self.simple_event_from_outlook_attendee.update(isOrganizer=False)
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Expected values for Outlook events converted to Odoo events
|
||||
# -----------------------------------------------------------------------------------------
|
||||
|
||||
self.expected_odoo_event_from_outlook = {
|
||||
"name": "simple_event",
|
||||
"description": Markup('<p>my simple event</p>'),
|
||||
"active": True,
|
||||
"start": self.start_date,
|
||||
"stop": self.end_date,
|
||||
"user_id": self.organizer_user,
|
||||
"microsoft_id": combine_ids("123", "456"),
|
||||
"partner_ids": [self.organizer_user.partner_id.id, self.attendee_user.partner_id.id],
|
||||
}
|
||||
self.expected_odoo_recurrency_from_outlook = {
|
||||
'active': True,
|
||||
'byday': '1',
|
||||
'count': 0,
|
||||
'day': 0,
|
||||
'display_name': "Every %s Days until %s" % (
|
||||
self.recurrent_event_interval, self.recurrence_end_date.strftime("%Y-%m-%d")
|
||||
),
|
||||
'dtstart': self.start_date,
|
||||
'end_type': 'end_date',
|
||||
'event_tz': False,
|
||||
'fri': False,
|
||||
'interval': self.recurrent_event_interval,
|
||||
'month_by': 'date',
|
||||
'microsoft_id': combine_ids('REC123', 'REC456'),
|
||||
'name': "Every %s Days until %s" % (
|
||||
self.recurrent_event_interval, self.recurrence_end_date.strftime("%Y-%m-%d")
|
||||
),
|
||||
'need_sync_m': False,
|
||||
'rrule': 'DTSTART:%s\nRRULE:FREQ=DAILY;INTERVAL=%s;UNTIL=%s' % (
|
||||
self.start_date.strftime("%Y%m%dT%H%M%S"),
|
||||
self.recurrent_event_interval,
|
||||
self.recurrence_end_date.strftime("%Y%m%dT235959"),
|
||||
),
|
||||
'rrule_type': 'daily',
|
||||
'until': self.recurrence_end_date.date(),
|
||||
'weekday': False,
|
||||
}
|
||||
|
||||
self.recurrent_event_from_outlook_organizer = [{
|
||||
'attendees': [{
|
||||
'emailAddress': {'address': self.attendee_user.email, 'name': self.attendee_user.display_name},
|
||||
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
|
||||
'type': 'required'
|
||||
}],
|
||||
'body': {
|
||||
'content': "my recurrent event",
|
||||
'contentType': "text",
|
||||
},
|
||||
'start': {'dateTime': self.start_date.strftime("%Y-%m-%dT%H:%M:%S.0000000"), 'timeZone': 'UTC'},
|
||||
'end': {'dateTime': self.end_date.strftime("%Y-%m-%dT%H:%M:%S.0000000"), 'timeZone': 'UTC'},
|
||||
'id': 'REC123',
|
||||
'iCalUId': 'REC456',
|
||||
'isAllDay': False,
|
||||
'isCancelled': False,
|
||||
'isOnlineMeeting': False,
|
||||
'isOrganizer': True,
|
||||
'isReminderOn': True,
|
||||
'location': {'displayName': ''},
|
||||
'organizer': {'emailAddress': {
|
||||
'address': self.organizer_user.email, 'name': self.organizer_user.display_name}
|
||||
},
|
||||
'recurrence': {
|
||||
'pattern': {
|
||||
'dayOfMonth': 0,
|
||||
'firstDayOfWeek': 'sunday',
|
||||
'index': 'first',
|
||||
'interval': self.recurrent_event_interval,
|
||||
'month': 0,
|
||||
'type': 'daily'
|
||||
},
|
||||
'range': {
|
||||
'startDate': self.start_date.strftime("%Y-%m-%d"),
|
||||
'endDate': self.recurrence_end_date.strftime("%Y-%m-%d"),
|
||||
'numberOfOccurrences': 0,
|
||||
'recurrenceTimeZone': 'Romance Standard Time',
|
||||
'type': 'endDate'
|
||||
}
|
||||
},
|
||||
'reminderMinutesBeforeStart': 15,
|
||||
'responseRequested': True,
|
||||
'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'},
|
||||
'sensitivity': 'normal',
|
||||
'seriesMasterId': None,
|
||||
'showAs': 'busy',
|
||||
'subject': "recurrent event",
|
||||
'type': 'seriesMaster',
|
||||
}]
|
||||
self.recurrent_event_from_outlook_organizer += [
|
||||
{
|
||||
'attendees': [{
|
||||
'emailAddress': {'address': self.attendee_user.email, 'name': self.attendee_user.display_name},
|
||||
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
|
||||
'type': 'required'
|
||||
}],
|
||||
'body': {
|
||||
'content': "my recurrent event",
|
||||
'contentType': "text",
|
||||
},
|
||||
'start': {
|
||||
'dateTime': (
|
||||
self.start_date + timedelta(days=i * self.recurrent_event_interval)
|
||||
).strftime("%Y-%m-%dT%H:%M:%S.0000000"),
|
||||
'timeZone': 'UTC'
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (
|
||||
self.end_date + timedelta(days=i * self.recurrent_event_interval)
|
||||
).strftime("%Y-%m-%dT%H:%M:%S.0000000"),
|
||||
'timeZone': 'UTC'
|
||||
},
|
||||
'id': f'REC123_EVENT_{i+1}',
|
||||
'iCalUId': f'REC456_EVENT_{i+1}',
|
||||
'seriesMasterId': 'REC123',
|
||||
'isAllDay': False,
|
||||
'isCancelled': False,
|
||||
'isOnlineMeeting': False,
|
||||
'isOrganizer': True,
|
||||
'isReminderOn': True,
|
||||
'location': {'displayName': ''},
|
||||
'organizer': {
|
||||
'emailAddress': {'address': self.organizer_user.email, 'name': self.organizer_user.display_name}
|
||||
},
|
||||
'recurrence': None,
|
||||
'reminderMinutesBeforeStart': 15,
|
||||
'responseRequested': True,
|
||||
'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'},
|
||||
'sensitivity': 'normal',
|
||||
'showAs': 'busy',
|
||||
'subject': "recurrent event",
|
||||
'type': 'occurrence',
|
||||
}
|
||||
for i in range(self.recurrent_events_count)
|
||||
]
|
||||
self.recurrent_event_from_outlook_attendee = [
|
||||
dict(
|
||||
d,
|
||||
isOrganizer=False,
|
||||
attendees=[
|
||||
{
|
||||
'emailAddress': {'address': self.organizer_user.email, 'name': self.organizer_user.display_name},
|
||||
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
|
||||
'type': 'required'
|
||||
},
|
||||
{
|
||||
'emailAddress': {'address': self.attendee_user.email, 'name': self.attendee_user.display_name},
|
||||
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
|
||||
'type': 'required'
|
||||
},
|
||||
]
|
||||
)
|
||||
for d in self.recurrent_event_from_outlook_organizer
|
||||
]
|
||||
|
||||
self.expected_odoo_recurrency_events_from_outlook = [
|
||||
{
|
||||
"name": "recurrent event",
|
||||
"user_id": self.organizer_user,
|
||||
"partner_ids": [self.organizer_user.partner_id.id, self.attendee_user.partner_id.id],
|
||||
"start": self.start_date + timedelta(days=i * self.recurrent_event_interval),
|
||||
"stop": self.end_date + timedelta(days=i * self.recurrent_event_interval),
|
||||
"until": self.recurrence_end_date.date(),
|
||||
"microsoft_recurrence_master_id": "REC123",
|
||||
'microsoft_id': combine_ids(f"REC123_EVENT_{i+1}", f"REC456_EVENT_{i+1}"),
|
||||
"recurrency": True,
|
||||
"follow_recurrence": True,
|
||||
"active": True,
|
||||
}
|
||||
for i in range(self.recurrent_events_count)
|
||||
]
|
||||
self.env.cr.postcommit.clear()
|
||||
|
||||
@contextmanager
|
||||
def mock_datetime_and_now(self, mock_dt):
|
||||
"""
|
||||
Used when synchronization date (using env.cr.now()) is important
|
||||
in addition to standard datetime mocks. Used mainly to detect sync
|
||||
issues.
|
||||
"""
|
||||
with freeze_time(mock_dt), \
|
||||
patch.object(self.env.cr, 'now', lambda: mock_dt):
|
||||
yield
|
||||
|
||||
def sync_odoo_recurrences_with_outlook_feature(self):
|
||||
"""
|
||||
Returns the status of the recurrence synchronization feature with Outlook.
|
||||
True if it is active and False otherwise. This function guides previous tests to abort before they are checked.
|
||||
"""
|
||||
return False
|
||||
|
||||
def create_events_for_tests(self):
|
||||
"""
|
||||
Create some events for test purpose
|
||||
"""
|
||||
|
||||
# ---- create some events that will be updated during tests -----
|
||||
|
||||
# a simple event
|
||||
self.simple_event = self.env["calendar.event"].search([("name", "=", "simple_event")])
|
||||
if not self.simple_event:
|
||||
self.simple_event = self.env["calendar.event"].with_user(self.organizer_user).create(
|
||||
dict(
|
||||
self.simple_event_values,
|
||||
microsoft_id=combine_ids("123", "456"),
|
||||
)
|
||||
)
|
||||
|
||||
# a group of events
|
||||
self.several_events = self.env["calendar.event"].search([("name", "like", "event%")])
|
||||
if not self.several_events:
|
||||
self.several_events = self.env["calendar.event"].with_user(self.organizer_user).create([
|
||||
dict(
|
||||
self.simple_event_values,
|
||||
name=f"event{i}",
|
||||
microsoft_id=combine_ids(f"e{i}", f"u{i}"),
|
||||
)
|
||||
for i in range(1, 4)
|
||||
])
|
||||
|
||||
# a recurrent event with 7 occurrences
|
||||
self.recurrent_base_event = self.env["calendar.event"].search(
|
||||
[("name", "=", "recurrent_event")],
|
||||
order="id",
|
||||
limit=1,
|
||||
)
|
||||
already_created = self.recurrent_base_event
|
||||
|
||||
# Currently, it is forbidden to create recurrences in Odoo. A trick for deactivating the checking
|
||||
# is needed below in this test setup: deactivating the synchronization during recurrences creation.
|
||||
sync_previous_state = self.env.user.microsoft_synchronization_stopped
|
||||
self.env.user.microsoft_synchronization_stopped = False
|
||||
|
||||
if not already_created:
|
||||
self.recurrent_base_event = self.env["calendar.event"].with_context(dont_notify=True).with_user(self.organizer_user).create(
|
||||
self.recurrent_event_values
|
||||
)
|
||||
self.recurrence = self.env["calendar.recurrence"].search([("base_event_id", "=", self.recurrent_base_event.id)])
|
||||
|
||||
# set ids set by Outlook
|
||||
if not already_created:
|
||||
self.recurrence.with_context(dont_notify=True).write({
|
||||
"microsoft_id": combine_ids("REC123", "REC456"),
|
||||
})
|
||||
for i, e in enumerate(self.recurrence.calendar_event_ids.sorted(key=lambda r: r.start)):
|
||||
e.with_context(dont_notify=True).write({
|
||||
"microsoft_id": combine_ids(f"REC123_EVENT_{i+1}", f"REC456_EVENT_{i+1}"),
|
||||
"microsoft_recurrence_master_id": "REC123",
|
||||
})
|
||||
self.recurrence.invalidate_recordset()
|
||||
self.recurrence.calendar_event_ids.invalidate_recordset()
|
||||
|
||||
self.recurrent_events = self.recurrence.calendar_event_ids.sorted(key=lambda r: r.start)
|
||||
self.recurrent_events_count = len(self.recurrent_events)
|
||||
|
||||
# Rollback the synchronization status after setup.
|
||||
self.env.user.microsoft_synchronization_stopped = sync_previous_state
|
||||
|
||||
def assert_odoo_event(self, odoo_event, expected_values):
|
||||
"""
|
||||
Assert that an Odoo event has the same values than in the expected_values dictionary,
|
||||
for the keys present in expected_values.
|
||||
"""
|
||||
self.assertTrue(expected_values)
|
||||
|
||||
odoo_event_values = odoo_event.read(list(expected_values.keys()))[0]
|
||||
for k, v in expected_values.items():
|
||||
if k in ("user_id", "recurrence_id"):
|
||||
v = (v.id, v.name) if v else False
|
||||
|
||||
if isinstance(v, list):
|
||||
self.assertListEqual(sorted(v), sorted(odoo_event_values.get(k)), msg=f"'{k}' mismatch")
|
||||
else:
|
||||
self.assertEqual(v, odoo_event_values.get(k), msg=f"'{k}' mismatch")
|
||||
|
||||
def assert_odoo_recurrence(self, odoo_recurrence, expected_values):
|
||||
"""
|
||||
Assert that an Odoo recurrence has the same values than in the expected_values dictionary,
|
||||
for the keys present in expected_values.
|
||||
"""
|
||||
odoo_recurrence_values = odoo_recurrence.read(list(expected_values.keys()))[0]
|
||||
|
||||
for k, v in expected_values.items():
|
||||
self.assertEqual(v, odoo_recurrence_values.get(k), msg=f"'{k}' mismatch")
|
||||
|
||||
def assert_dict_equal(self, dict1, dict2):
|
||||
|
||||
# check missing keys
|
||||
keys = set(dict1.keys()) ^ set(dict2.keys())
|
||||
self.assertFalse(keys, msg="Following keys are not in both dicts: %s" % ", ".join(keys))
|
||||
|
||||
# compare key by key
|
||||
for k, v in dict1.items():
|
||||
self.assertEqual(v, dict2.get(k), f"'{k}' mismatch")
|
||||
|
||||
def call_post_commit_hooks(self):
|
||||
"""
|
||||
manually calls postcommit hooks defined with the decorator @after_commit
|
||||
"""
|
||||
|
||||
# need to manually handle post-commit hooks calls as `self.env.cr.postcommit.run()` clean
|
||||
# the queue at the end of the first post-commit hook call ...
|
||||
funcs = self.env.cr.postcommit._funcs.copy()
|
||||
while funcs:
|
||||
func = funcs.popleft()
|
||||
func()
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch, ANY
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
|
||||
from odoo.addons.microsoft_calendar.models.res_users import User
|
||||
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
|
||||
from odoo.addons.microsoft_calendar.tests.common import TestCommon, mock_get_token, _modified_date_in_the_future, patch_api
|
||||
from odoo.tests import users
|
||||
|
||||
import json
|
||||
from freezegun import freeze_time
|
||||
|
||||
|
||||
@patch.object(User, '_get_microsoft_calendar_token', mock_get_token)
|
||||
class TestAnswerEvents(TestCommon):
|
||||
|
||||
@patch_api
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# a simple event
|
||||
self.simple_event = self.env["calendar.event"].search([("name", "=", "simple_event")])
|
||||
if not self.simple_event:
|
||||
self.simple_event = self.env["calendar.event"].with_user(self.organizer_user).create(
|
||||
dict(
|
||||
self.simple_event_values,
|
||||
microsoft_id=combine_ids("123", "456"),
|
||||
)
|
||||
)
|
||||
(self.organizer_user | self.attendee_user).microsoft_calendar_token_validity = datetime.now() + timedelta(hours=1)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, '_get_single_event')
|
||||
@patch.object(MicrosoftCalendarService, 'answer')
|
||||
def test_attendee_accepts_event_from_odoo_calendar(self, mock_answer, mock_get_single_event):
|
||||
attendee = self.env["calendar.attendee"].search([
|
||||
('event_id', '=', self.simple_event.id),
|
||||
('partner_id', '=', self.attendee_user.partner_id.id)
|
||||
])
|
||||
attendee_ms_organizer_event_id = 100
|
||||
mock_get_single_event.return_value = (True, {'value': [{'id': attendee_ms_organizer_event_id}]})
|
||||
attendee.with_user(self.attendee_user).do_accept()
|
||||
self.call_post_commit_hooks()
|
||||
self.simple_event.invalidate_recordset()
|
||||
|
||||
mock_answer.assert_called_once_with(
|
||||
attendee_ms_organizer_event_id,
|
||||
'accept',
|
||||
{"comment": "", "sendResponse": True},
|
||||
token=mock_get_token(self.attendee_user),
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, '_get_single_event')
|
||||
@patch.object(MicrosoftCalendarService, 'answer')
|
||||
def test_attendee_declines_event_from_odoo_calendar(self, mock_answer, mock_get_single_event):
|
||||
attendee = self.env["calendar.attendee"].search([
|
||||
('event_id', '=', self.simple_event.id),
|
||||
('partner_id', '=', self.attendee_user.partner_id.id)
|
||||
])
|
||||
attendee_ms_organizer_event_id = 100
|
||||
mock_get_single_event.return_value = (True, {'value': [{'id': attendee_ms_organizer_event_id}]})
|
||||
attendee.with_user(self.attendee_user).do_decline()
|
||||
self.call_post_commit_hooks()
|
||||
self.simple_event.invalidate_recordset()
|
||||
mock_answer.assert_called_once_with(
|
||||
attendee_ms_organizer_event_id,
|
||||
'decline',
|
||||
{"comment": "", "sendResponse": True},
|
||||
token=mock_get_token(self.attendee_user),
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
@freeze_time('2021-09-22')
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_attendee_accepts_event_from_outlook_calendar(self, mock_get_events):
|
||||
"""
|
||||
In his Outlook calendar, the attendee accepts the event and sync with his odoo calendar.
|
||||
"""
|
||||
mock_get_events.return_value = (
|
||||
MicrosoftEvent([dict(
|
||||
self.simple_event_from_outlook_organizer,
|
||||
attendees=[{
|
||||
'type': 'required',
|
||||
'status': {'response': 'accepted', 'time': '0001-01-01T00:00:00Z'},
|
||||
'emailAddress': {'name': self.attendee_user.display_name, 'address': self.attendee_user.email}
|
||||
}],
|
||||
lastModifiedDateTime=_modified_date_in_the_future(self.simple_event)
|
||||
)]), None
|
||||
)
|
||||
self.attendee_user.with_user(self.attendee_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
attendee = self.env["calendar.attendee"].search([
|
||||
('event_id', '=', self.simple_event.id),
|
||||
('partner_id', '=', self.attendee_user.partner_id.id)
|
||||
])
|
||||
self.assertEqual(attendee.state, "accepted")
|
||||
|
||||
@freeze_time('2021-09-22')
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_attendee_accepts_event_from_outlook_calendar_synced_by_organizer(self, mock_get_events):
|
||||
"""
|
||||
In his Outlook calendar, the attendee accepts the event and the organizer syncs his odoo calendar.
|
||||
"""
|
||||
mock_get_events.return_value = (
|
||||
MicrosoftEvent([dict(
|
||||
self.simple_event_from_outlook_organizer,
|
||||
attendees=[{
|
||||
'type': 'required',
|
||||
'status': {'response': 'accepted', 'time': '0001-01-01T00:00:00Z'},
|
||||
'emailAddress': {'name': self.attendee_user.display_name, 'address': self.attendee_user.email}
|
||||
}],
|
||||
lastModifiedDateTime=_modified_date_in_the_future(self.simple_event)
|
||||
)]), None
|
||||
)
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
attendee = self.env["calendar.attendee"].search([
|
||||
('event_id', '=', self.simple_event.id),
|
||||
('partner_id', '=', self.attendee_user.partner_id.id)
|
||||
])
|
||||
self.assertEqual(attendee.state, "accepted")
|
||||
|
||||
def test_attendee_declines_event_from_outlook_calendar(self):
|
||||
"""
|
||||
In his Outlook calendar, the attendee declines the event leading to automatically
|
||||
delete this event (that's the way Outlook handles it ...)
|
||||
|
||||
LIMITATION:
|
||||
|
||||
But, as there is no way to get the iCalUId to identify the corresponding Odoo event,
|
||||
there is no way to update the attendee status to "declined".
|
||||
"""
|
||||
|
||||
@freeze_time('2021-09-22')
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_attendee_declines_event_from_outlook_calendar_synced_by_organizer(self, mock_get_events):
|
||||
"""
|
||||
In his Outlook calendar, the attendee declines the event leading to automatically
|
||||
delete this event (that's the way Outlook handles it ...)
|
||||
"""
|
||||
mock_get_events.return_value = (
|
||||
MicrosoftEvent([dict(
|
||||
self.simple_event_from_outlook_organizer,
|
||||
attendees=[{
|
||||
'type': 'required',
|
||||
'status': {'response': 'declined', 'time': '0001-01-01T00:00:00Z'},
|
||||
'emailAddress': {'name': self.attendee_user.display_name, 'address': self.attendee_user.email}
|
||||
}],
|
||||
lastModifiedDateTime=_modified_date_in_the_future(self.simple_event)
|
||||
)]), None
|
||||
)
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
attendee = self.env["calendar.attendee"].search([
|
||||
('event_id', '=', self.simple_event.id),
|
||||
('partner_id', '=', self.attendee_user.partner_id.id)
|
||||
])
|
||||
self.assertEqual(attendee.state, "declined")
|
||||
|
||||
@users('admin')
|
||||
def test_sync_data_with_stopped_sync(self):
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'microsoft_calendar_client_id',
|
||||
'test_microsoft_calendar_client_id'
|
||||
)
|
||||
self.env.user.sudo().microsoft_calendar_rtoken = 'test_microsoft_calendar_rtoken'
|
||||
self.env.user.stop_microsoft_synchronization()
|
||||
payload = {
|
||||
'params': {
|
||||
'model': 'calendar.event'
|
||||
}
|
||||
}
|
||||
# Sending the request to the sync_data
|
||||
response = self.url_open(
|
||||
'/microsoft_calendar/sync_data',
|
||||
data=json.dumps(payload),
|
||||
headers={'Content-Type': 'application/json'}
|
||||
).json()
|
||||
# the status must be sync_stopped
|
||||
self.assertEqual(response['result']['status'], 'sync_stopped')
|
||||
|
||||
@patch.object(MicrosoftCalendarService, '_get_single_event')
|
||||
@patch.object(MicrosoftCalendarService, 'answer')
|
||||
def test_answer_event_with_external_organizer(self, mock_answer, mock_get_single_event):
|
||||
""" Answer an event invitation from an outsider user and check if it was patched on Outlook side. """
|
||||
# Simulate an event that came from an external provider: the organizer isn't registered in Odoo.
|
||||
self.simple_event.write({'user_id': False, 'partner_id': False})
|
||||
self.simple_event.attendee_ids.state = 'needsAction'
|
||||
|
||||
# Accept the event using the admin account and ensure that answer request is called.
|
||||
attendee_ms_organizer_event_id = 100
|
||||
mock_get_single_event.return_value = (True, {'value': [{'id': attendee_ms_organizer_event_id}]})
|
||||
self.simple_event.attendee_ids[0].with_user(self.organizer_user)._microsoft_sync_event('accept')
|
||||
mock_answer.assert_called_once_with(
|
||||
attendee_ms_organizer_event_id,
|
||||
'accept', {'comment': '', 'sendResponse': True},
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=20
|
||||
)
|
||||
|
||||
# Decline the event using the admin account and ensure that answer request is called.
|
||||
self.simple_event.attendee_ids[0].with_user(self.organizer_user)._microsoft_sync_event('decline')
|
||||
mock_answer.assert_called_with(
|
||||
attendee_ms_organizer_event_id,
|
||||
'decline', {'comment': '', 'sendResponse': True},
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=20
|
||||
)
|
||||
|
|
@ -0,0 +1,485 @@
|
|||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command, fields
|
||||
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
|
||||
from odoo.addons.microsoft_calendar.models.res_users import User
|
||||
from odoo.addons.microsoft_calendar.tests.common import TestCommon, mock_get_token
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
@patch.object(User, '_get_microsoft_calendar_token', mock_get_token)
|
||||
class TestCreateEvents(TestCommon):
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'insert')
|
||||
def test_create_simple_event_without_sync(self, mock_insert):
|
||||
"""
|
||||
A Odoo event is created when Outlook sync is not enabled.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
self.organizer_user.microsoft_synchronization_stopped = True
|
||||
|
||||
# act
|
||||
record = self.env["calendar.event"].with_user(self.organizer_user).create(self.simple_event_values)
|
||||
self.call_post_commit_hooks()
|
||||
record.invalidate_recordset()
|
||||
|
||||
# assert
|
||||
mock_insert.assert_not_called()
|
||||
self.assertEqual(record.need_sync_m, False)
|
||||
|
||||
def test_create_simple_event_without_email(self):
|
||||
"""
|
||||
Outlook does not accept attendees without email.
|
||||
"""
|
||||
# arrange
|
||||
self.attendee_user.partner_id.email = False
|
||||
|
||||
# act & assert
|
||||
record = self.env["calendar.event"].with_user(self.organizer_user).create(self.simple_event_values)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
record._sync_odoo2microsoft()
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_create_simple_event_from_outlook_organizer_calendar(self, mock_get_events):
|
||||
"""
|
||||
An event has been created in Outlook and synced in the Odoo organizer calendar.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
mock_get_events.return_value = (MicrosoftEvent([self.simple_event_from_outlook_organizer]), None)
|
||||
existing_records = self.env["calendar.event"].search([])
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
records = self.env["calendar.event"].search([])
|
||||
new_records = (records - existing_records)
|
||||
self.assertEqual(len(new_records), 1)
|
||||
self.assert_odoo_event(new_records, self.expected_odoo_event_from_outlook)
|
||||
self.assertEqual(new_records.user_id, self.organizer_user)
|
||||
self.assertEqual(new_records.need_sync_m, False)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_create_simple_event_from_outlook_attendee_calendar_and_organizer_exists_in_odoo(self, mock_get_events):
|
||||
"""
|
||||
An event has been created in Outlook and synced in the Odoo attendee calendar.
|
||||
There is a Odoo user that matches with the organizer email address.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
mock_get_events.return_value = (MicrosoftEvent([self.simple_event_from_outlook_attendee]), None)
|
||||
existing_records = self.env["calendar.event"].search([])
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
records = self.env["calendar.event"].search([])
|
||||
new_records = (records - existing_records)
|
||||
self.assertEqual(len(new_records), 1)
|
||||
self.assert_odoo_event(new_records, self.expected_odoo_event_from_outlook)
|
||||
self.assertEqual(new_records.user_id, self.organizer_user)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_create_simple_event_from_outlook_attendee_calendar_and_organizer_does_not_exist_in_odoo(self, mock_get_events):
|
||||
"""
|
||||
An event has been created in Outlook and synced in the Odoo attendee calendar.
|
||||
no Odoo user that matches with the organizer email address.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
outlook_event = self.simple_event_from_outlook_attendee
|
||||
outlook_event = dict(self.simple_event_from_outlook_attendee, organizer={
|
||||
'emailAddress': {'address': "john.doe@odoo.com", 'name': "John Doe"},
|
||||
})
|
||||
expected_event = dict(self.expected_odoo_event_from_outlook, user_id=False)
|
||||
|
||||
mock_get_events.return_value = (MicrosoftEvent([outlook_event]), None)
|
||||
existing_records = self.env["calendar.event"].search([])
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
records = self.env["calendar.event"].search([])
|
||||
new_records = (records - existing_records)
|
||||
self.assertEqual(len(new_records), 1)
|
||||
self.assert_odoo_event(new_records, expected_event)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_create_simple_event_from_outlook_attendee_calendar_where_email_addresses_are_capitalized(self, mock_get_events):
|
||||
"""
|
||||
An event has been created in Outlook and synced in the Odoo attendee calendar.
|
||||
The email addresses of the attendee and the organizer are in different case than in Odoo.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
outlook_event = dict(self.simple_event_from_outlook_attendee, organizer={
|
||||
'emailAddress': {'address': "Mike@organizer.com", 'name': "Mike Organizer"},
|
||||
}, attendees=[{'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
|
||||
'emailAddress': {'name': 'John Attendee', 'address': 'John@attendee.com'}}])
|
||||
|
||||
mock_get_events.return_value = (MicrosoftEvent([outlook_event]), None)
|
||||
existing_records = self.env["calendar.event"].search([])
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
records = self.env["calendar.event"].search([])
|
||||
new_records = (records - existing_records)
|
||||
self.assertEqual(len(new_records), 1)
|
||||
self.assert_odoo_event(new_records, self.expected_odoo_event_from_outlook)
|
||||
self.assertEqual(new_records.user_id, self.organizer_user)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'insert')
|
||||
def test_create_recurrent_event_without_sync(self, mock_insert):
|
||||
"""
|
||||
A Odoo recurrent event is created when Outlook sync is not enabled.
|
||||
"""
|
||||
if not self.sync_odoo_recurrences_with_outlook_feature():
|
||||
return
|
||||
|
||||
# arrange
|
||||
self.organizer_user.microsoft_synchronization_stopped = True
|
||||
|
||||
# act
|
||||
record = self.env["calendar.event"].with_user(self.organizer_user).create(self.recurrent_event_values)
|
||||
self.call_post_commit_hooks()
|
||||
record.invalidate_recordset()
|
||||
|
||||
# assert
|
||||
mock_insert.assert_not_called()
|
||||
self.assertEqual(record.need_sync_m, False)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
@patch.object(MicrosoftCalendarService, 'insert')
|
||||
def test_create_recurrent_event_with_sync(self, mock_insert, mock_get_events):
|
||||
"""
|
||||
A Odoo recurrent event is created when Outlook sync is enabled.
|
||||
"""
|
||||
if not self.sync_odoo_recurrences_with_outlook_feature():
|
||||
return
|
||||
|
||||
# >>> first phase: create the recurrence
|
||||
|
||||
# act
|
||||
record = self.env["calendar.event"].with_user(self.organizer_user).create(self.recurrent_event_values)
|
||||
|
||||
# assert
|
||||
recurrence = self.env["calendar.recurrence"].search([("base_event_id", "=", record.id)])
|
||||
|
||||
mock_insert.assert_not_called()
|
||||
self.assertEqual(record.name, "recurring_event")
|
||||
self.assertEqual(recurrence.name, "Every 2 Days for 7 events")
|
||||
self.assertEqual(len(recurrence.calendar_event_ids), 7)
|
||||
|
||||
# >>> second phase: sync with organizer outlook calendar
|
||||
|
||||
# arrange
|
||||
event_id = "123"
|
||||
event_iCalUId = "456"
|
||||
mock_insert.return_value = (event_id, event_iCalUId)
|
||||
mock_get_events.return_value = ([], None)
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
self.call_post_commit_hooks()
|
||||
recurrence.invalidate_recordset()
|
||||
|
||||
# assert
|
||||
self.assertEqual(recurrence.ms_organizer_event_id, event_id)
|
||||
self.assertEqual(recurrence.ms_universal_event_id, event_iCalUId)
|
||||
self.assertEqual(recurrence.need_sync_m, False)
|
||||
|
||||
mock_insert.assert_called_once()
|
||||
self.assert_dict_equal(mock_insert.call_args[0][0], self.recurrent_event_ms_values)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
@patch.object(MicrosoftCalendarService, 'insert')
|
||||
def test_create_recurrent_event_with_sync_by_another_user(self, mock_insert, mock_get_events):
|
||||
"""
|
||||
A Odoo recurrent event has been created and synced with Outlook by another user, but nothing
|
||||
should happen as it we prevent sync of recurrences from other users
|
||||
( see microsoft_calendar/models/calendar_recurrence_rule.py::_get_microsoft_sync_domain() )
|
||||
"""
|
||||
if not self.sync_odoo_recurrences_with_outlook_feature():
|
||||
return
|
||||
# >>> first phase: create the recurrence
|
||||
|
||||
# act
|
||||
record = self.env["calendar.event"].with_user(self.organizer_user).create(self.recurrent_event_values)
|
||||
|
||||
# assert
|
||||
recurrence = self.env["calendar.recurrence"].search([("base_event_id", "=", record.id)])
|
||||
|
||||
mock_insert.assert_not_called()
|
||||
self.assertEqual(record.name, "recurring_event")
|
||||
self.assertEqual(recurrence.name, f"Every 2 Days for {self.recurrent_events_count} events")
|
||||
self.assertEqual(len(recurrence.calendar_event_ids), self.recurrent_events_count)
|
||||
|
||||
# >>> second phase: sync with attendee Outlook calendar
|
||||
|
||||
# arrange
|
||||
event_id = "123"
|
||||
event_iCalUId = "456"
|
||||
mock_insert.return_value = (event_id, event_iCalUId)
|
||||
mock_get_events.return_value = ([], None)
|
||||
|
||||
# act
|
||||
self.attendee_user.with_user(self.attendee_user).sudo()._sync_microsoft_calendar()
|
||||
self.call_post_commit_hooks()
|
||||
recurrence.invalidate_recordset()
|
||||
|
||||
# assert
|
||||
mock_insert.assert_not_called()
|
||||
|
||||
self.assertEqual(recurrence.ms_organizer_event_id, False)
|
||||
self.assertEqual(recurrence.ms_universal_event_id, False)
|
||||
self.assertEqual(recurrence.need_sync_m, False)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_create_recurrent_event_from_outlook_organizer_calendar(self, mock_get_events):
|
||||
"""
|
||||
A recurrent event has been created in Outlook and synced in the Odoo organizer calendar.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
mock_get_events.return_value = (MicrosoftEvent(self.recurrent_event_from_outlook_organizer), None)
|
||||
existing_events = self.env["calendar.event"].search([])
|
||||
existing_recurrences = self.env["calendar.recurrence"].search([])
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
new_events = (self.env["calendar.event"].search([]) - existing_events)
|
||||
new_recurrences = (self.env["calendar.recurrence"].search([]) - existing_recurrences)
|
||||
self.assertEqual(len(new_recurrences), 1)
|
||||
self.assertEqual(len(new_events), self.recurrent_events_count)
|
||||
self.assert_odoo_recurrence(new_recurrences, self.expected_odoo_recurrency_from_outlook)
|
||||
for i, e in enumerate(sorted(new_events, key=lambda e: e.id)):
|
||||
self.assert_odoo_event(e, self.expected_odoo_recurrency_events_from_outlook[i])
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_create_recurrent_event_from_outlook_attendee_calendar(self, mock_get_events):
|
||||
"""
|
||||
A recurrent event has been created in Outlook and synced in the Odoo attendee calendar.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
mock_get_events.return_value = (MicrosoftEvent(self.recurrent_event_from_outlook_attendee), None)
|
||||
existing_events = self.env["calendar.event"].search([])
|
||||
existing_recurrences = self.env["calendar.recurrence"].search([])
|
||||
|
||||
# act
|
||||
self.attendee_user.with_user(self.attendee_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
new_events = (self.env["calendar.event"].search([]) - existing_events)
|
||||
new_recurrences = (self.env["calendar.recurrence"].search([]) - existing_recurrences)
|
||||
self.assertEqual(len(new_recurrences), 1)
|
||||
self.assertEqual(len(new_events), self.recurrent_events_count)
|
||||
self.assert_odoo_recurrence(new_recurrences, self.expected_odoo_recurrency_from_outlook)
|
||||
for i, e in enumerate(sorted(new_events, key=lambda e: e.id)):
|
||||
self.assert_odoo_event(e, self.expected_odoo_recurrency_events_from_outlook[i])
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'insert')
|
||||
def test_forbid_recurrences_creation_synced_outlook_calendar(self, mock_insert):
|
||||
"""
|
||||
Forbids new recurrences creation in Odoo due to Outlook spam limitation of updating recurrent events.
|
||||
"""
|
||||
# Set custom calendar token validity to simulate real scenario.
|
||||
self.env.user.microsoft_calendar_token_validity = datetime.now() + timedelta(minutes=5)
|
||||
|
||||
# Assert that synchronization with Outlook is active.
|
||||
self.assertFalse(self.env.user.microsoft_synchronization_stopped)
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
self.env["calendar.event"].create(
|
||||
self.recurrent_event_values
|
||||
)
|
||||
# Assert that no insert call was made.
|
||||
mock_insert.assert_not_called()
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
@patch.object(MicrosoftCalendarService, 'insert')
|
||||
def test_create_event_for_another_user(self, mock_insert, mock_get_events):
|
||||
"""
|
||||
Allow the creation of event for another user only if the proposed user have its Odoo Calendar synced.
|
||||
User A (self.organizer_user) is creating an event with user B as organizer (self.attendee_user).
|
||||
"""
|
||||
# Ensure that the calendar synchronization of user A is active. Deactivate user B synchronization for throwing an error.
|
||||
self.assertTrue(self.env['calendar.event'].with_user(self.organizer_user)._check_microsoft_sync_status())
|
||||
self.attendee_user.microsoft_synchronization_stopped = True
|
||||
|
||||
# Try creating an event with the organizer as the user B (self.attendee_user).
|
||||
# A ValidationError must be thrown because user B's calendar is not synced.
|
||||
self.simple_event_values['user_id'] = self.attendee_user.id
|
||||
self.simple_event_values['partner_ids'] = [Command.set([self.organizer_user.partner_id.id])]
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
|
||||
|
||||
# Activate the calendar synchronization of user B (self.attendee_user).
|
||||
self.attendee_user.microsoft_synchronization_stopped = False
|
||||
self.assertTrue(self.env['calendar.event'].with_user(self.attendee_user)._check_microsoft_sync_status())
|
||||
|
||||
# Try creating an event with organizer as the user B but not inserting B as an attendee. A ValidationError must be thrown.
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
|
||||
|
||||
# Set mock return values for the event creation.
|
||||
event_id = "123"
|
||||
event_iCalUId = "456"
|
||||
mock_insert.return_value = (event_id, event_iCalUId)
|
||||
mock_get_events.return_value = ([], None)
|
||||
|
||||
# Create event matching the creation conditions: user B is synced and now listed as an attendee. Set mock return values.
|
||||
self.simple_event_values['partner_ids'] = [Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])]
|
||||
event = self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
|
||||
self.call_post_commit_hooks()
|
||||
event.invalidate_recordset()
|
||||
|
||||
# Ensure that event was inserted and user B (self.attendee_user) is the organizer and is also listed as attendee.
|
||||
mock_insert.assert_called_once()
|
||||
self.assertEqual(event.user_id, self.attendee_user, "Event organizer must be user B (self.attendee_user) after event creation by user A (self.organizer_user).")
|
||||
self.assertTrue(self.attendee_user.partner_id.id in event.partner_ids.ids, "User B (self.attendee_user) should be listed as attendee after event creation.")
|
||||
|
||||
# Try creating an event with portal user (with no access rights) as organizer from Microsoft.
|
||||
# In Odoo, this event will be created (behind the screens) by a synced Odoo user as attendee (self.attendee_user).
|
||||
portal_group = self.env.ref('base.group_portal')
|
||||
portal_user = self.env['res.users'].create({
|
||||
'login': 'portal@user',
|
||||
'email': 'portal@user',
|
||||
'name': 'PortalUser',
|
||||
'groups_id': [Command.set([portal_group.id])],
|
||||
})
|
||||
|
||||
# Mock event from Microsoft and sync event with Odoo through self.attendee_user (synced user).
|
||||
self.simple_event_from_outlook_organizer.update({
|
||||
'id': 'portalUserEventID',
|
||||
'iCalUId': 'portalUserEventICalUId',
|
||||
'organizer': {'emailAddress': {'address': portal_user.login, 'name': portal_user.name}},
|
||||
})
|
||||
mock_get_events.return_value = (MicrosoftEvent([self.simple_event_from_outlook_organizer]), None)
|
||||
self.assertTrue(self.env['calendar.event'].with_user(self.attendee_user)._check_microsoft_sync_status())
|
||||
self.attendee_user.with_user(self.attendee_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# Ensure that event was successfully created in Odoo (no ACL error was triggered blocking creation).
|
||||
portal_user_events = self.env['calendar.event'].search([('user_id', '=', portal_user.id)])
|
||||
self.assertEqual(len(portal_user_events), 1)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_create_simple_event_from_outlook_without_organizer(self, mock_get_events):
|
||||
"""
|
||||
Allow creation of an event without organizer in Outlook and sync it in Odoo.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
outlook_event = self.simple_event_from_outlook_attendee
|
||||
outlook_event = dict(self.simple_event_from_outlook_attendee, organizer=None)
|
||||
expected_event = dict(self.expected_odoo_event_from_outlook, user_id=False)
|
||||
|
||||
mock_get_events.return_value = (MicrosoftEvent([outlook_event]), None)
|
||||
existing_records = self.env["calendar.event"].search([])
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
records = self.env["calendar.event"].search([])
|
||||
new_records = (records - existing_records)
|
||||
self.assertEqual(len(new_records), 1)
|
||||
self.assert_odoo_event(new_records, expected_event)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
@patch.object(MicrosoftCalendarService, 'insert')
|
||||
def test_new_db_skip_odoo2microsoft_sync_previously_created_events(self, mock_insert, mock_get_events):
|
||||
"""
|
||||
Skip the synchronization of previously created events if the database never synchronized with
|
||||
Outlook Calendar before. This is necessary for avoiding spamming lots of invitations in the first
|
||||
synchronization of users. A single ICP parameter 'first_synchronization_date' is shared in the DB
|
||||
to save down the first synchronization date of any of all users.
|
||||
"""
|
||||
# During preparation: ensure that no user ever synchronized with Outlook Calendar
|
||||
# and create a local event waiting to be synchronized (need_sync_m: True).
|
||||
with self.mock_datetime_and_now('2024-01-01 10:00:00'):
|
||||
any_calendar_synchronized = self.env['res.users'].sudo().search_count(
|
||||
domain=[('microsoft_calendar_sync_token', '!=', False)],
|
||||
limit=1
|
||||
)
|
||||
self.assertFalse(any_calendar_synchronized)
|
||||
self.organizer_user.microsoft_synchronization_stopped = True
|
||||
event = self.env['calendar.event'].with_user(self.organizer_user).create({
|
||||
'name': "Odoo Local Event",
|
||||
'start': datetime(2024, 1, 1, 11, 0),
|
||||
'stop': datetime(2024, 1, 1, 13, 0),
|
||||
'user_id': self.organizer_user.id,
|
||||
'partner_ids': [(4, self.organizer_user.partner_id.id)],
|
||||
'need_sync_m': True
|
||||
})
|
||||
|
||||
# For simulating a real world scenario, save the first synchronization date
|
||||
# one day later after creating the event that won't be synchronized.
|
||||
self.organizer_user._set_ICP_first_synchronization_date(
|
||||
fields.Datetime.from_string('2024-01-02 10:00:00')
|
||||
)
|
||||
|
||||
# Ten seconds later the ICP parameter saving, make the synchronization between Odoo
|
||||
# and Outlook and ensure that insert was not called, i.e. the event got skipped.
|
||||
with self.mock_datetime_and_now('2024-01-02 10:00:10'):
|
||||
# Mock the return of 0 events from Outlook to Odoo, then activate the user's sync.
|
||||
mock_get_events.return_value = ([], None)
|
||||
self.organizer_user.microsoft_synchronization_stopped = False
|
||||
self.organizer_user.microsoft_calendar_token_validity = datetime.now() + timedelta(minutes=60)
|
||||
self.assertTrue(self.env['calendar.event'].with_user(self.organizer_user)._check_microsoft_sync_status())
|
||||
|
||||
# Synchronize the user's calendar and call post commit hooks for analyzing the API calls.
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
self.call_post_commit_hooks()
|
||||
event.invalidate_recordset()
|
||||
mock_insert.assert_not_called()
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
@patch.object(MicrosoftCalendarService, 'insert')
|
||||
def test_old_db_odoo2microsoft_sync_previously_created_events(self, mock_insert, mock_get_events):
|
||||
"""
|
||||
Ensure that existing databases that are already synchronized with Outlook Calendar at some point
|
||||
won't skip any events creation in Outlook side during the first synchronization of the users.
|
||||
This test is important to make sure the behavior won't be changed for existing production envs.
|
||||
"""
|
||||
# During preparation: ensure that the organizer is not synchronized with Outlook and
|
||||
# create a local event waiting to be synchronized (need_sync_m: True) without API calls.
|
||||
with self.mock_datetime_and_now('2024-01-01 10:00:00'):
|
||||
self.organizer_user.microsoft_synchronization_stopped = True
|
||||
event = self.env['calendar.event'].with_user(self.organizer_user).create({
|
||||
'name': "Odoo Local Event",
|
||||
'start': datetime(2024, 1, 1, 11, 0),
|
||||
'stop': datetime(2024, 1, 1, 13, 0),
|
||||
'user_id': self.organizer_user.id,
|
||||
'partner_ids': [(4, self.organizer_user.partner_id.id)],
|
||||
'need_sync_m': True
|
||||
})
|
||||
|
||||
# Assign a next sync token to ANY user to simulate a previous sync in the DB.
|
||||
self.attendee_user.microsoft_calendar_sync_token = 'OngoingToken'
|
||||
|
||||
# Mock the return of 0 events from Outlook to Odoo, then activate the user's sync.
|
||||
mock_get_events.return_value = ([], None)
|
||||
mock_insert.return_value = ('LocalEventSyncID', 'event_iCalUId')
|
||||
self.organizer_user.microsoft_synchronization_stopped = False
|
||||
self.organizer_user.microsoft_calendar_token_validity = datetime.now() + timedelta(minutes=60)
|
||||
self.assertTrue(self.env['calendar.event'].with_user(self.organizer_user)._check_microsoft_sync_status())
|
||||
|
||||
# Synchronize the user's calendar and call post commit hooks for analyzing the API calls.
|
||||
# Our event must be synchronized normally in the first synchronization of the user.
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
self.call_post_commit_hooks()
|
||||
event.invalidate_recordset()
|
||||
mock_insert.assert_called_once()
|
||||
self.assertEqual(mock_insert.call_args[0][0]['subject'], event.name)
|
||||
|
|
@ -0,0 +1,338 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch, ANY, call
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import fields
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
|
||||
from odoo.addons.microsoft_calendar.models.res_users import User
|
||||
from odoo.addons.microsoft_calendar.tests.common import (
|
||||
TestCommon,
|
||||
mock_get_token,
|
||||
_modified_date_in_the_future,
|
||||
patch_api
|
||||
)
|
||||
|
||||
@patch.object(User, '_get_microsoft_calendar_token', mock_get_token)
|
||||
class TestDeleteEvents(TestCommon):
|
||||
|
||||
@patch_api
|
||||
def setUp(self):
|
||||
super(TestDeleteEvents, self).setUp()
|
||||
self.create_events_for_tests()
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_delete_simple_event_from_odoo_organizer_calendar(self, mock_delete):
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
|
||||
self.simple_event.with_user(self.organizer_user).unlink()
|
||||
self.call_post_commit_hooks()
|
||||
self.simple_event.invalidate_recordset()
|
||||
|
||||
self.assertFalse(self.simple_event.exists())
|
||||
mock_delete.assert_called_once_with(
|
||||
event_id,
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=ANY
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_delete_simple_event_from_odoo_attendee_calendar(self, mock_delete):
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
|
||||
self.simple_event.with_user(self.attendee_user).unlink()
|
||||
self.call_post_commit_hooks()
|
||||
self.simple_event.invalidate_recordset()
|
||||
|
||||
self.assertFalse(self.simple_event.exists())
|
||||
mock_delete.assert_called_once_with(
|
||||
event_id,
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=ANY
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_archive_simple_event_from_odoo_organizer_calendar(self, mock_delete):
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
|
||||
self.simple_event.with_user(self.organizer_user).write({'active': False})
|
||||
self.call_post_commit_hooks()
|
||||
self.simple_event.invalidate_recordset()
|
||||
|
||||
self.assertTrue(self.simple_event.exists())
|
||||
self.assertFalse(self.simple_event.active)
|
||||
mock_delete.assert_called_once_with(
|
||||
event_id,
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=ANY
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_archive_simple_event_from_odoo_attendee_calendar(self, mock_delete):
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
|
||||
self.simple_event.with_user(self.attendee_user).write({'active': False})
|
||||
self.call_post_commit_hooks()
|
||||
self.simple_event.invalidate_recordset()
|
||||
|
||||
self.assertTrue(self.simple_event.exists())
|
||||
self.assertFalse(self.simple_event.active)
|
||||
mock_delete.assert_called_once_with(
|
||||
event_id,
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=ANY
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_archive_several_events_at_once(self, mock_delete):
|
||||
"""
|
||||
Archive several events at once should not produce any exception.
|
||||
"""
|
||||
# arrange
|
||||
several_simple_events = self.several_events.filtered(lambda ev: not ev.recurrency and ev.microsoft_id)
|
||||
# act
|
||||
several_simple_events.action_archive()
|
||||
self.call_post_commit_hooks()
|
||||
several_simple_events.invalidate_recordset()
|
||||
|
||||
# assert
|
||||
self.assertFalse(all(e.active for e in several_simple_events))
|
||||
|
||||
mock_delete.assert_has_calls([
|
||||
call(e.ms_organizer_event_id, token=ANY, timeout=ANY)
|
||||
for e in several_simple_events
|
||||
])
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_cancel_simple_event_from_outlook_organizer_calendar(self, mock_get_events):
|
||||
"""
|
||||
In his Outlook calendar, the organizer cannot delete the event, he can only cancel it.
|
||||
"""
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
mock_get_events.return_value = (
|
||||
MicrosoftEvent([{
|
||||
"id": event_id,
|
||||
"@removed": {"reason": "deleted"}
|
||||
}]),
|
||||
None
|
||||
)
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
self.assertFalse(self.simple_event.exists())
|
||||
|
||||
def test_delete_simple_event_from_outlook_attendee_calendar(self):
|
||||
"""
|
||||
If an attendee deletes an event from its Outlook calendar, during the sync, Odoo will be notified that
|
||||
this event has been deleted BUT only with the attendees's calendar event id and not with the global one
|
||||
(called iCalUId). That means, it's not possible to match this deleted event with an Odoo event.
|
||||
|
||||
LIMITATION:
|
||||
|
||||
Unfortunately, there is no magic solution:
|
||||
1) keep the list of calendar events ids linked to a unique iCalUId but all Odoo users may not have synced
|
||||
their Odoo calendar, leading to missing ids in the list => bad solution.
|
||||
2) call the microsoft API to get the iCalUId matching the received event id => as the event has already
|
||||
been deleted, this call may return an error.
|
||||
"""
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_delete_one_event_from_recurrence_from_odoo_calendar(self, mock_delete):
|
||||
if not self.sync_odoo_recurrences_with_outlook_feature():
|
||||
return
|
||||
# arrange
|
||||
idx = 2
|
||||
event_id = self.recurrent_events[idx].ms_organizer_event_id
|
||||
|
||||
# act
|
||||
self.recurrent_events[idx].with_user(self.organizer_user).unlink()
|
||||
self.call_post_commit_hooks()
|
||||
|
||||
# assert
|
||||
self.assertFalse(self.recurrent_events[idx].exists())
|
||||
self.assertEqual(len(self.recurrence.calendar_event_ids), self.recurrent_events_count - 1)
|
||||
mock_delete.assert_called_once_with(
|
||||
event_id,
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=ANY
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_delete_first_event_from_recurrence_from_odoo_calendar(self, mock_delete):
|
||||
if not self.sync_odoo_recurrences_with_outlook_feature():
|
||||
return
|
||||
# arrange
|
||||
idx = 0
|
||||
event_id = self.recurrent_events[idx].ms_organizer_event_id
|
||||
|
||||
# act
|
||||
self.recurrent_events[idx].with_user(self.organizer_user).unlink()
|
||||
self.call_post_commit_hooks()
|
||||
|
||||
# assert
|
||||
self.assertFalse(self.recurrent_events[idx].exists())
|
||||
self.assertEqual(len(self.recurrence.calendar_event_ids), self.recurrent_events_count - 1)
|
||||
self.assertEqual(self.recurrence.base_event_id, self.recurrent_events[1])
|
||||
mock_delete.assert_called_once_with(
|
||||
event_id,
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=ANY
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_delete_one_event_from_recurrence_from_outlook_calendar(self, mock_get_events):
|
||||
"""
|
||||
When a single event is removed from a recurrence, Outlook returns the recurrence and
|
||||
events which still exist.
|
||||
"""
|
||||
# arrange
|
||||
idx = 3
|
||||
rec_values = [
|
||||
dict(
|
||||
event,
|
||||
lastModifiedDateTime=_modified_date_in_the_future(self.recurrence)
|
||||
)
|
||||
for i, event in enumerate(self.recurrent_event_from_outlook_organizer)
|
||||
if i != (idx + 1) # + 1 because recurrent_event_from_outlook_organizer contains the recurrence itself as first item
|
||||
]
|
||||
event_to_remove = self.recurrent_events[idx]
|
||||
mock_get_events.return_value = (MicrosoftEvent(rec_values), None)
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
self.assertFalse(event_to_remove.exists())
|
||||
self.assertEqual(len(self.recurrence.calendar_event_ids), self.recurrent_events_count - 1)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_delete_first_event_from_recurrence_from_outlook_calendar(self, mock_get_events):
|
||||
# arrange
|
||||
rec_values = [
|
||||
dict(
|
||||
event,
|
||||
lastModifiedDateTime=_modified_date_in_the_future(self.recurrence)
|
||||
)
|
||||
for i, event in enumerate(self.recurrent_event_from_outlook_organizer)
|
||||
if i != 1
|
||||
]
|
||||
event_to_remove = self.recurrent_events[0]
|
||||
next_base_event = self.recurrent_events[1]
|
||||
mock_get_events.return_value = (MicrosoftEvent(rec_values), None)
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
self.assertFalse(event_to_remove.exists())
|
||||
self.assertEqual(len(self.recurrence.calendar_event_ids), self.recurrent_events_count - 1)
|
||||
self.assertEqual(self.recurrence.base_event_id, next_base_event)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_delete_one_event_and_future_from_recurrence_from_outlook_calendar(self, mock_get_events):
|
||||
if not self.sync_odoo_recurrences_with_outlook_feature():
|
||||
return
|
||||
# arrange
|
||||
idx = range(4, self.recurrent_events_count)
|
||||
rec_values = [
|
||||
dict(
|
||||
event,
|
||||
lastModifiedDateTime=_modified_date_in_the_future(self.recurrence)
|
||||
)
|
||||
for i, event in enumerate(self.recurrent_event_from_outlook_organizer)
|
||||
if i not in [x + 1 for x in idx]
|
||||
]
|
||||
event_to_remove = [e for i, e in enumerate(self.recurrent_events) if i in idx]
|
||||
mock_get_events.return_value = (MicrosoftEvent(rec_values), None)
|
||||
|
||||
# act
|
||||
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
for e in event_to_remove:
|
||||
self.assertFalse(e.exists())
|
||||
self.assertEqual(len(self.recurrence.calendar_event_ids), self.recurrent_events_count - len(idx))
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_delete_first_event_and_future_from_recurrence_from_outlook_calendar(self, mock_get_events):
|
||||
"""
|
||||
In Outlook, deleting the first event and future ones is the same than removing all the recurrence.
|
||||
"""
|
||||
# arrange
|
||||
mock_get_events.return_value = (
|
||||
MicrosoftEvent([{
|
||||
"id": self.recurrence.ms_organizer_event_id,
|
||||
"@removed": {"reason": "deleted"}
|
||||
}]),
|
||||
None
|
||||
)
|
||||
|
||||
# act
|
||||
self.organizer_user.with_context(dont_notify=True).with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
|
||||
|
||||
# assert
|
||||
self.assertFalse(self.recurrence.exists())
|
||||
self.assertFalse(self.recurrence.calendar_event_ids.exists())
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'get_events')
|
||||
def test_delete_all_events_from_recurrence_from_outlook_calendar(self, mock_get_events):
|
||||
"""
|
||||
Same than test_delete_first_event_and_future_from_recurrence_from_outlook_calendar.
|
||||
"""
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_delete_single_event_from_recurrence_from_odoo_calendar(self, mock_delete):
|
||||
"""
|
||||
Deletes the base_event of a recurrence and checks if the event was archived and the recurrence was updated.
|
||||
"""
|
||||
if not self.sync_odoo_recurrences_with_outlook_feature():
|
||||
return
|
||||
# arrange
|
||||
idx = 0
|
||||
event_id = self.recurrent_events[idx].ms_organizer_event_id
|
||||
|
||||
# act
|
||||
self.recurrent_events[idx].with_user(self.organizer_user).action_mass_archive('self_only')
|
||||
self.call_post_commit_hooks()
|
||||
|
||||
# assert that event is not active anymore and that a new base_event was select for the recurrence
|
||||
self.assertFalse(self.recurrent_events[idx].active)
|
||||
self.assertNotEqual(self.id, self.recurrent_events[idx].recurrence_id.base_event_id.id)
|
||||
self.assertTrue(self.id not in [rec.id for rec in self.recurrent_events[idx].recurrence_id.calendar_event_ids])
|
||||
mock_delete.assert_called_once_with(
|
||||
event_id,
|
||||
token=mock_get_token(self.organizer_user),
|
||||
timeout=ANY
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, 'delete')
|
||||
def test_delete_recurrence_previously_synced(self, mock_delete):
|
||||
# Arrange: select recurrent event and update token validity to simulate an active sync environment.
|
||||
idx = 0
|
||||
self.organizer_user.microsoft_calendar_token_validity = fields.Datetime.now() + timedelta(hours=1)
|
||||
|
||||
# Act: try to delete a recurrent event that was already synced.
|
||||
with self.assertRaises(UserError):
|
||||
self.recurrent_events[idx].with_user(self.organizer_user).action_mass_archive('all_events')
|
||||
self.call_post_commit_hooks()
|
||||
|
||||
# Ensure that event remains undeleted after deletion attempt and delete method wasn't called.
|
||||
self.assertTrue(self.recurrent_events[idx].with_user(self.organizer_user)._check_microsoft_sync_status())
|
||||
self.assertTrue(self.recurrent_events[idx].active)
|
||||
mock_delete.assert_not_called()
|
||||
|
||||
def test_forbid_recurrence_unlinking_list_view(self):
|
||||
# Forbid recurrence unlinking from list view with sync on.
|
||||
self.assertTrue(self.env['calendar.event'].with_user(self.organizer_user)._check_microsoft_sync_status())
|
||||
with self.assertRaises(UserError):
|
||||
self.recurrent_events.unlink()
|
||||
|
||||
# Allow recurrence unlinking when update comes from Microsoft (dont_notify=True).
|
||||
self.recurrent_events[2:].with_context(dont_notify=True).unlink()
|
||||
self.assertTrue(all(not event.exists() for event in self.recurrent_events[2:]), "Recurrent event must be deleted after unlink from Microsoft.")
|
||||
|
||||
# Allow unlinking recurrence when sync is off for the current user.
|
||||
self.organizer_user.microsoft_synchronization_stopped = True
|
||||
self.assertFalse(self.env['calendar.event'].with_user(self.organizer_user)._check_microsoft_sync_status())
|
||||
self.recurrent_events[1].with_user(self.organizer_user).unlink()
|
||||
self.assertFalse(self.recurrent_events[1].exists(), "Recurrent event must be deleted after unlink with sync off.")
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from pytz import UTC
|
||||
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
|
||||
from odoo.addons.microsoft_calendar.tests.common import TestCommon, patch_api
|
||||
|
||||
class TestMicrosoftEvent(TestCommon):
|
||||
|
||||
@patch_api
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.create_events_for_tests()
|
||||
|
||||
def test_already_mapped_events(self):
|
||||
|
||||
# arrange
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
event_uid = self.simple_event.ms_universal_event_id
|
||||
events = MicrosoftEvent([{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": self.simple_event.id,
|
||||
"iCalUId": event_uid,
|
||||
"id": event_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
mapped = events._load_odoo_ids_from_db(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(mapped._events), 1)
|
||||
self.assertEqual(mapped._events[event_id]["_odoo_id"], self.simple_event.id)
|
||||
|
||||
def test_map_an_event_using_global_id(self):
|
||||
|
||||
# arrange
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
event_uid = self.simple_event.ms_universal_event_id
|
||||
events = MicrosoftEvent([{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": event_uid,
|
||||
"id": event_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
mapped = events._load_odoo_ids_from_db(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(mapped._events), 1)
|
||||
self.assertEqual(mapped._events[event_id]["_odoo_id"], self.simple_event.id)
|
||||
|
||||
def test_map_an_event_using_instance_id(self):
|
||||
"""
|
||||
Here, the Odoo event has an uid but the Outlook event has not.
|
||||
"""
|
||||
# arrange
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
events = MicrosoftEvent([{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": False,
|
||||
"id": event_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
mapped = events._load_odoo_ids_from_db(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(mapped._events), 1)
|
||||
self.assertEqual(mapped._events[event_id]["_odoo_id"], self.simple_event.id)
|
||||
|
||||
def test_map_an_event_without_uid_using_instance_id(self):
|
||||
"""
|
||||
Here, the Odoo event has no uid but the Outlook event has one.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
event_uid = self.simple_event.ms_universal_event_id
|
||||
self.simple_event.ms_universal_event_id = False
|
||||
events = MicrosoftEvent([{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": event_uid,
|
||||
"id": event_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
mapped = events._load_odoo_ids_from_db(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(mapped._events), 1)
|
||||
self.assertEqual(mapped._events[event_id]["_odoo_id"], self.simple_event.id)
|
||||
self.assertEqual(self.simple_event.ms_universal_event_id, event_uid)
|
||||
|
||||
def test_map_an_event_without_uid_using_instance_id_2(self):
|
||||
"""
|
||||
Here, both Odoo event and Outlook event have no uid.
|
||||
"""
|
||||
|
||||
# arrange
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
self.simple_event.ms_universal_event_id = False
|
||||
events = MicrosoftEvent([{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": False,
|
||||
"id": event_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
mapped = events._load_odoo_ids_from_db(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(mapped._events), 1)
|
||||
self.assertEqual(mapped._events[event_id]["_odoo_id"], self.simple_event.id)
|
||||
self.assertEqual(self.simple_event.ms_universal_event_id, False)
|
||||
|
||||
def test_map_a_recurrence_using_global_id(self):
|
||||
|
||||
# arrange
|
||||
rec_id = self.recurrence.ms_organizer_event_id
|
||||
rec_uid = self.recurrence.ms_universal_event_id
|
||||
events = MicrosoftEvent([{
|
||||
"type": "seriesMaster",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": rec_uid,
|
||||
"id": rec_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
mapped = events._load_odoo_ids_from_db(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(mapped._events), 1)
|
||||
self.assertEqual(mapped._events[rec_id]["_odoo_id"], self.recurrence.id)
|
||||
|
||||
def test_map_a_recurrence_using_instance_id(self):
|
||||
|
||||
# arrange
|
||||
rec_id = self.recurrence.ms_organizer_event_id
|
||||
events = MicrosoftEvent([{
|
||||
"type": "seriesMaster",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": False,
|
||||
"id": rec_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
mapped = events._load_odoo_ids_from_db(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(mapped._events), 1)
|
||||
self.assertEqual(mapped._events[rec_id]["_odoo_id"], self.recurrence.id)
|
||||
|
||||
def test_try_to_map_mixed_of_single_events_and_recurrences(self):
|
||||
|
||||
# arrange
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
event_uid = self.simple_event.ms_universal_event_id
|
||||
rec_id = self.recurrence.ms_organizer_event_id
|
||||
rec_uid = self.recurrence.ms_universal_event_id
|
||||
|
||||
events = MicrosoftEvent([
|
||||
{
|
||||
"type": "seriesMaster",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": rec_uid,
|
||||
"id": rec_id,
|
||||
},
|
||||
{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": event_uid,
|
||||
"id": event_id,
|
||||
},
|
||||
])
|
||||
|
||||
# act & assert
|
||||
with self.assertRaises(TypeError):
|
||||
events._load_odoo_ids_from_db(self.env)
|
||||
|
||||
def test_match_event_only(self):
|
||||
|
||||
# arrange
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
event_uid = self.simple_event.ms_universal_event_id
|
||||
events = MicrosoftEvent([{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": event_uid,
|
||||
"id": event_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
matched = events.match_with_odoo_events(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(matched._events), 1)
|
||||
self.assertEqual(matched._events[event_id]["_odoo_id"], self.simple_event.id)
|
||||
|
||||
def test_match_recurrence_only(self):
|
||||
|
||||
# arrange
|
||||
rec_id = self.recurrence.ms_organizer_event_id
|
||||
rec_uid = self.recurrence.ms_universal_event_id
|
||||
events = MicrosoftEvent([{
|
||||
"type": "seriesMaster",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": rec_uid,
|
||||
"id": rec_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
matched = events.match_with_odoo_events(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(matched._events), 1)
|
||||
self.assertEqual(matched._events[rec_id]["_odoo_id"], self.recurrence.id)
|
||||
|
||||
def test_match_not_typed_recurrence(self):
|
||||
"""
|
||||
When a recurrence is deleted, Outlook returns the id of the deleted recurrence
|
||||
without the type of event, so it's not directly possible to know that it's a
|
||||
recurrence.
|
||||
"""
|
||||
# arrange
|
||||
rec_id = self.recurrence.ms_organizer_event_id
|
||||
rec_uid = self.recurrence.ms_universal_event_id
|
||||
events = MicrosoftEvent([{
|
||||
"@removed": {
|
||||
"reason": "deleted",
|
||||
},
|
||||
"_odoo_id": False,
|
||||
"iCalUId": rec_uid,
|
||||
"id": rec_id,
|
||||
}])
|
||||
|
||||
# act
|
||||
matched = events.match_with_odoo_events(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(matched._events), 1)
|
||||
self.assertEqual(matched._events[rec_id]["_odoo_id"], self.recurrence.id)
|
||||
|
||||
def test_match_mix_of_events_and_recurrences(self):
|
||||
|
||||
# arrange
|
||||
event_id = self.simple_event.ms_organizer_event_id
|
||||
event_uid = self.simple_event.ms_universal_event_id
|
||||
rec_id = self.recurrence.ms_organizer_event_id
|
||||
rec_uid = self.recurrence.ms_universal_event_id
|
||||
|
||||
events = MicrosoftEvent([
|
||||
{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": event_uid,
|
||||
"id": event_id,
|
||||
},
|
||||
{
|
||||
"@removed": {
|
||||
"reason": "deleted",
|
||||
},
|
||||
"_odoo_id": False,
|
||||
"iCalUId": rec_uid,
|
||||
"id": rec_id,
|
||||
}
|
||||
])
|
||||
|
||||
# act
|
||||
matched = events.match_with_odoo_events(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(matched._events), 2)
|
||||
self.assertEqual(matched._events[event_id]["_odoo_id"], self.simple_event.id)
|
||||
self.assertEqual(matched._events[rec_id]["_odoo_id"], self.recurrence.id)
|
||||
|
||||
def test_ignore_not_found_items(self):
|
||||
|
||||
# arrange
|
||||
events = MicrosoftEvent([{
|
||||
"type": "singleInstance",
|
||||
"_odoo_id": False,
|
||||
"iCalUId": "UNKNOWN_EVENT",
|
||||
"id": "UNKNOWN_EVENT",
|
||||
}])
|
||||
|
||||
# act
|
||||
matched = events.match_with_odoo_events(self.env)
|
||||
|
||||
# assert
|
||||
self.assertEqual(len(matched._events), 0)
|
||||
|
||||
def test_search_set_ms_universal_event_id(self):
|
||||
not_synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '=', False)])
|
||||
synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '!=', False)])
|
||||
|
||||
self.assertIn(self.simple_event, synced_events)
|
||||
self.assertNotIn(self.simple_event, not_synced_events)
|
||||
|
||||
self.simple_event.ms_universal_event_id = ''
|
||||
not_synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '=', False)])
|
||||
synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '!=', False)])
|
||||
|
||||
self.assertNotIn(self.simple_event, synced_events)
|
||||
self.assertIn(self.simple_event, not_synced_events)
|
||||
|
||||
def test_microsoft_event_readonly(self):
|
||||
event = MicrosoftEvent()
|
||||
with self.assertRaises(TypeError):
|
||||
event._events['foo'] = 'bar'
|
||||
with self.assertRaises(AttributeError):
|
||||
event._events.update({'foo': 'bar'})
|
||||
with self.assertRaises(TypeError):
|
||||
dict.update(event._events, {'foo': 'bar'})
|
||||
|
||||
def test_performance_check(self):
|
||||
# Test what happens when microsoft returns a lot of data
|
||||
# This test does not aim to check what we do with the data but it ensure that we are able to process it.
|
||||
# Other tests take care of how we update odoo records with the api result.
|
||||
|
||||
start_date = datetime(2023, 9, 25, 17, 25)
|
||||
record_count = 10000
|
||||
single_event_data = [{
|
||||
'@odata.type': '#microsoft.graph.event',
|
||||
'@odata.etag': f'W/"AAAAAA{x}"',
|
||||
'type': 'singleInstance',
|
||||
'createdDateTime': (start_date + relativedelta(minutes=x)).isoformat(),
|
||||
'lastModifiedDateTime': (datetime.now().astimezone(UTC) + relativedelta(days=3)).isoformat(),
|
||||
'changeKey': f'ZS2uEVAVyU6BMZ3m6cH{x}mtgAADI/Dig==',
|
||||
'categories': [],
|
||||
'originalStartTimeZone': 'Romance Standard Time',
|
||||
'originalEndTimeZone': 'Romance Standard Time',
|
||||
'id': f'AA{x}',
|
||||
'subject': f"Subject of {x}",
|
||||
'bodyPreview': f"Body of {x}",
|
||||
'start': {'dateTime': (start_date + relativedelta(minutes=x)).isoformat(), 'timeZone': 'UTC'},
|
||||
'end': {'dateTime': (start_date + relativedelta(minutes=x)).isoformat(), 'timeZone': 'UTC'},
|
||||
'isOrganizer': True,
|
||||
'organizer': {'emailAddress': {'name': f'outlook_{x}@outlook.com', 'address': f'outlook_{x}@outlook.com'}},
|
||||
} for x in range(record_count)]
|
||||
|
||||
events = MicrosoftEvent(single_event_data)
|
||||
mapped = events._load_odoo_ids_from_db(self.env)
|
||||
self.assertFalse(mapped, "No odoo record should correspond to the microsoft values")
|
||||
|
||||
recurring_event_data = [{
|
||||
'@odata.type': '#microsoft.graph.event',
|
||||
'@odata.etag': f'W/"{x}IaZKQ=="',
|
||||
'createdDateTime': (start_date + relativedelta(minutes=(2*x))).isoformat(),
|
||||
'lastModifiedDateTime': (datetime.now().astimezone(UTC) + relativedelta(days=3)).isoformat(),
|
||||
'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==',
|
||||
'categories': [],
|
||||
'originalStartTimeZone': 'Romance Standard Time',
|
||||
'originalEndTimeZone': 'Romance Standard Time',
|
||||
'iCalUId': f'XX{x}',
|
||||
'id': f'AAA{x}',
|
||||
'reminderMinutesBeforeStart': 15,
|
||||
'isReminderOn': True,
|
||||
'hasAttachments': False,
|
||||
'subject': f'My recurrent event {x}',
|
||||
'bodyPreview': '', 'importance':
|
||||
'normal', 'sensitivity': 'normal',
|
||||
'isAllDay': False, 'isCancelled': False,
|
||||
'isOrganizer': True, 'IsRoomRequested': False,
|
||||
'AutoRoomBookingStatus': 'None',
|
||||
'responseRequested': True,
|
||||
'seriesMasterId': None,
|
||||
'showAs': 'busy',
|
||||
'type': 'seriesMaster',
|
||||
'webLink': f'https://outlook.live.com/owa/?itemid={x}&exvsurl=1&path=/calendar/item',
|
||||
'onlineMeetingUrl': None,
|
||||
'isOnlineMeeting': False,
|
||||
'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True,
|
||||
'IsDraft': False,
|
||||
'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'},
|
||||
'body': {'contentType': 'html', 'content': ''},
|
||||
'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'},
|
||||
'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'},
|
||||
'location': {'displayName': '',
|
||||
'locationType': 'default',
|
||||
'uniqueIdType': 'unknown',
|
||||
'address': {},
|
||||
'coordinates': {}},
|
||||
'locations': [],
|
||||
'recurrence': {'pattern':
|
||||
{'type': 'daily',
|
||||
'interval': 1,
|
||||
'month': 0,
|
||||
'dayOfMonth': 0,
|
||||
'firstDayOfWeek': 'sunday',
|
||||
'index': 'first'},
|
||||
'range': {'type': 'endDate',
|
||||
'startDate': '2020-05-03',
|
||||
'endDate': '2020-05-05',
|
||||
'recurrenceTimeZone': 'Romance Standard Time',
|
||||
'numberOfOccurrences': 0}
|
||||
},
|
||||
'attendees': [],
|
||||
'organizer': {'emailAddress': {'name': f'outlook_{x}@outlook.com',
|
||||
'address': f'outlook_{x}@outlook.com'}}
|
||||
} for x in range(record_count)]
|
||||
|
||||
recurrences = MicrosoftEvent(recurring_event_data)
|
||||
mapped = recurrences._load_odoo_ids_from_db(self.env)
|
||||
self.assertFalse(mapped, "No odoo record should correspond to the microsoft values")
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
import json
|
||||
import requests
|
||||
from unittest.mock import patch, call, MagicMock
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
|
||||
from odoo.addons.microsoft_account.models.microsoft_service import MicrosoftService
|
||||
from odoo.tests import TransactionCase
|
||||
|
||||
|
||||
DEFAULT_TIMEOUT = 20
|
||||
|
||||
|
||||
class TestMicrosoftService(TransactionCase):
|
||||
|
||||
def _do_request_result(self, data):
|
||||
""" _do_request returns a tuple (status, data, time) but only the data part is used """
|
||||
return (None, data, None)
|
||||
|
||||
def setUp(self):
|
||||
super(TestMicrosoftService, self).setUp()
|
||||
|
||||
self.service = MicrosoftCalendarService(self.env["microsoft.service"])
|
||||
self.fake_token = "MY_TOKEN"
|
||||
self.fake_sync_token = "MY_SYNC_TOKEN"
|
||||
self.fake_next_sync_token = "MY_NEXT_SYNC_TOKEN"
|
||||
self.fake_next_sync_token_url = f"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken={self.fake_next_sync_token}"
|
||||
|
||||
self.header_prefer = 'outlook.body-content-type="html", odata.maxpagesize=50'
|
||||
self.header = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % self.fake_token}
|
||||
self.call_with_sync_token = call(
|
||||
"/v1.0/me/calendarView/delta",
|
||||
{"$deltatoken": self.fake_sync_token},
|
||||
{**self.header, 'Prefer': self.header_prefer},
|
||||
method="GET", timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
self.call_without_sync_token = call(
|
||||
"/v1.0/me/calendarView/delta",
|
||||
{
|
||||
'startDateTime': fields.Datetime.subtract(fields.Datetime.now(), days=365).strftime("%Y-%m-%dT00:00:00Z"),
|
||||
'endDateTime': fields.Datetime.add(fields.Datetime.now(), days=365 * 2).strftime("%Y-%m-%dT00:00:00Z"),
|
||||
},
|
||||
{**self.header, 'Prefer': self.header_prefer},
|
||||
method="GET", timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
def test_get_events_delta_without_token(self):
|
||||
"""
|
||||
if no token is provided, an exception is raised
|
||||
"""
|
||||
with self.assertRaises(AttributeError):
|
||||
self.service._get_events_delta()
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_unexpected_exception(self, mock_do_request):
|
||||
"""
|
||||
When an unexpected exception is raised, just propagate it.
|
||||
"""
|
||||
mock_do_request.side_effect = Exception()
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
self.service._get_events_delta(token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, "_check_full_sync_required")
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_delta_token_error(self, mock_do_request, mock_check_full_sync_required):
|
||||
"""
|
||||
When the provided sync token is invalid, an exception should be raised and then
|
||||
a full sync should be done.
|
||||
"""
|
||||
mock_do_request.side_effect = [
|
||||
requests.HTTPError(response=MagicMock(status_code=410, content="fullSyncRequired")),
|
||||
self._do_request_result({"value": []}),
|
||||
]
|
||||
mock_check_full_sync_required.return_value = (True)
|
||||
|
||||
events, next_token = self.service._get_events_delta(
|
||||
token=self.fake_token, sync_token=self.fake_sync_token, timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
self.assertEqual(next_token, None)
|
||||
self.assertFalse(events)
|
||||
mock_do_request.assert_has_calls([self.call_with_sync_token, self.call_without_sync_token])
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_delta_without_sync_token(self, mock_do_request):
|
||||
"""
|
||||
when no sync token is provided, a full sync should be done
|
||||
"""
|
||||
# returns empty data without any next sync token
|
||||
mock_do_request.return_value = self._do_request_result({"value": []})
|
||||
|
||||
events, next_token = self.service._get_events_delta(token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertEqual(next_token, None)
|
||||
self.assertFalse(events)
|
||||
mock_do_request.assert_has_calls([self.call_without_sync_token])
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_delta_with_sync_token(self, mock_do_request):
|
||||
"""
|
||||
when a sync token is provided, we should retrieve the sync token to use for the next sync.
|
||||
"""
|
||||
# returns empty data with a next sync token
|
||||
mock_do_request.return_value = self._do_request_result({
|
||||
"value": [],
|
||||
"@odata.deltaLink": self.fake_next_sync_token_url
|
||||
})
|
||||
|
||||
events, next_token = self.service._get_events_delta(
|
||||
token=self.fake_token, sync_token=self.fake_sync_token, timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
self.assertEqual(next_token, "MY_NEXT_SYNC_TOKEN")
|
||||
self.assertFalse(events)
|
||||
mock_do_request.assert_has_calls([self.call_with_sync_token])
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_one_page(self, mock_do_request):
|
||||
"""
|
||||
When all events are on one page, just get them.
|
||||
"""
|
||||
mock_do_request.return_value = self._do_request_result({
|
||||
"value": [
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "singleInstance", "subject": "ev2"},
|
||||
{"id": 3, "type": "singleInstance", "subject": "ev3"},
|
||||
],
|
||||
})
|
||||
events, _ = self.service._get_events_delta(token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertEqual(events, MicrosoftEvent([
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "singleInstance", "subject": "ev2"},
|
||||
{"id": 3, "type": "singleInstance", "subject": "ev3"},
|
||||
]))
|
||||
mock_do_request.assert_has_calls([self.call_without_sync_token])
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_loop_over_pages(self, mock_do_request):
|
||||
"""
|
||||
Loop over pages to retrieve all the events.
|
||||
"""
|
||||
mock_do_request.side_effect = [
|
||||
self._do_request_result({
|
||||
"value": [{"id": 1, "type": "singleInstance", "subject": "ev1"}],
|
||||
"@odata.nextLink": "link_1"
|
||||
}),
|
||||
self._do_request_result({
|
||||
"value": [{"id": 2, "type": "singleInstance", "subject": "ev2"}],
|
||||
"@odata.nextLink": "link_2"
|
||||
}),
|
||||
self._do_request_result({
|
||||
"value": [{"id": 3, "type": "singleInstance", "subject": "ev3"}],
|
||||
}),
|
||||
]
|
||||
|
||||
events, _ = self.service._get_events_delta(token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertEqual(events, MicrosoftEvent([
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "singleInstance", "subject": "ev2"},
|
||||
{"id": 3, "type": "singleInstance", "subject": "ev3"},
|
||||
]))
|
||||
mock_do_request.assert_has_calls([
|
||||
self.call_without_sync_token,
|
||||
call(
|
||||
"link_1",
|
||||
{},
|
||||
{**self.header, 'Prefer': self.header_prefer},
|
||||
preuri='', method="GET", timeout=DEFAULT_TIMEOUT
|
||||
),
|
||||
call(
|
||||
"link_2",
|
||||
{},
|
||||
{**self.header, 'Prefer': self.header_prefer},
|
||||
preuri='', method="GET", timeout=DEFAULT_TIMEOUT
|
||||
),
|
||||
])
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_filter_out_occurrences(self, mock_do_request):
|
||||
"""
|
||||
When all events are on one page, just get them.
|
||||
"""
|
||||
mock_do_request.return_value = self._do_request_result({
|
||||
"value": [
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "occurrence", "subject": "ev2"},
|
||||
{"id": 3, "type": "seriesMaster", "subject": "ev3"},
|
||||
],
|
||||
})
|
||||
events, _ = self.service._get_events_delta(token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertEqual(events, MicrosoftEvent([
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 3, "type": "seriesMaster", "subject": "ev3"},
|
||||
]))
|
||||
mock_do_request.assert_has_calls([self.call_without_sync_token])
|
||||
|
||||
def test_get_occurrence_details_token_error(self):
|
||||
"""
|
||||
if no token is provided, an exception is raised
|
||||
"""
|
||||
with self.assertRaises(AttributeError):
|
||||
self.service._get_occurrence_details(1)
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_occurrence_details(self, mock_do_request):
|
||||
mock_do_request.return_value = self._do_request_result({
|
||||
"value": [
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "occurrence", "subject": "ev2"},
|
||||
{"id": 3, "type": "seriesMaster", "subject": "ev3"},
|
||||
],
|
||||
})
|
||||
events = self.service._get_occurrence_details(123, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertEqual(events, MicrosoftEvent([
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "occurrence", "subject": "ev2"},
|
||||
{"id": 3, "type": "seriesMaster", "subject": "ev3"},
|
||||
]))
|
||||
|
||||
mock_do_request.assert_called_with(
|
||||
"/v1.0/me/events/123/instances",
|
||||
{
|
||||
'startDateTime': fields.Datetime.subtract(fields.Datetime.now(), days=365).strftime("%Y-%m-%dT00:00:00Z"),
|
||||
'endDateTime': fields.Datetime.add(fields.Datetime.now(), days=365 * 2).strftime("%Y-%m-%dT00:00:00Z"),
|
||||
},
|
||||
{**self.header, 'Prefer': self.header_prefer},
|
||||
method='GET', timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
def test_get_events_token_error(self):
|
||||
"""
|
||||
if no token is provided, an exception is raised
|
||||
"""
|
||||
with self.assertRaises(AttributeError):
|
||||
self.service.get_events()
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_no_serie_master(self, mock_do_request):
|
||||
"""
|
||||
When there is no serie master, just retrieve the list of events.
|
||||
"""
|
||||
mock_do_request.return_value = self._do_request_result({
|
||||
"value": [
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "singleInstance", "subject": "ev2"},
|
||||
{"id": 3, "type": "singleInstance", "subject": "ev3"},
|
||||
],
|
||||
})
|
||||
|
||||
events, _ = self.service.get_events(token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertEqual(events, MicrosoftEvent([
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "singleInstance", "subject": "ev2"},
|
||||
{"id": 3, "type": "singleInstance", "subject": "ev3"},
|
||||
]))
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_with_one_serie_master(self, mock_do_request):
|
||||
"""
|
||||
When there is a serie master, retrieve the list of events and event occurrences linked to the serie master
|
||||
"""
|
||||
mock_do_request.side_effect = [
|
||||
self._do_request_result({
|
||||
"value": [
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "seriesMaster", "subject": "ev2"},
|
||||
],
|
||||
}),
|
||||
self._do_request_result({
|
||||
"value": [
|
||||
{"id": 3, "type": "occurrence", "subject": "ev3"},
|
||||
],
|
||||
}),
|
||||
]
|
||||
|
||||
events, _ = self.service.get_events(token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertEqual(events, MicrosoftEvent([
|
||||
{"id": 1, "type": "singleInstance", "subject": "ev1"},
|
||||
{"id": 2, "type": "seriesMaster", "subject": "ev2"},
|
||||
{"id": 3, "type": "occurrence", "subject": "ev3"},
|
||||
]))
|
||||
|
||||
def test_insert_token_error(self):
|
||||
"""
|
||||
if no token is provided, an exception is raised
|
||||
"""
|
||||
with self.assertRaises(AttributeError):
|
||||
self.service.insert({})
|
||||
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_insert(self, mock_do_request):
|
||||
|
||||
mock_do_request.return_value = self._do_request_result({'id': 1, 'iCalUId': 2})
|
||||
|
||||
instance_id, event_id = self.service.insert({"subject": "ev1"}, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertEqual(instance_id, 1)
|
||||
self.assertEqual(event_id, 2)
|
||||
mock_do_request.assert_called_with(
|
||||
"/v1.0/me/calendar/events",
|
||||
json.dumps({"subject": "ev1"}),
|
||||
self.header, method="POST", timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
def test_patch_token_error(self):
|
||||
"""
|
||||
if no token is provided, an exception is raised
|
||||
"""
|
||||
with self.assertRaises(AttributeError):
|
||||
self.service.patch(123, {})
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_patch_returns_false_if_event_does_not_exist(self, mock_do_request):
|
||||
event_id = 123
|
||||
values = {"subject": "ev2"}
|
||||
mock_do_request.return_value = (404, "", None)
|
||||
|
||||
res = self.service.patch(event_id, values, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertFalse(res)
|
||||
mock_do_request.assert_called_with(
|
||||
f"/v1.0/me/calendar/events/{event_id}",
|
||||
json.dumps(values),
|
||||
self.header, method="PATCH", timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_patch_an_existing_event(self, mock_do_request):
|
||||
event_id = 123
|
||||
values = {"subject": "ev2"}
|
||||
mock_do_request.return_value = (200, "", None)
|
||||
|
||||
res = self.service.patch(event_id, values, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertTrue(res)
|
||||
mock_do_request.assert_called_with(
|
||||
f"/v1.0/me/calendar/events/{event_id}",
|
||||
json.dumps(values),
|
||||
self.header, method="PATCH", timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
def test_delete_token_error(self):
|
||||
"""
|
||||
if no token is provided, an exception is raised
|
||||
"""
|
||||
with self.assertRaises(AttributeError):
|
||||
self.service.delete(123)
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_delete_returns_false_if_event_does_not_exist(self, mock_do_request):
|
||||
event_id = 123
|
||||
mock_do_request.return_value = (404, "", None)
|
||||
|
||||
res = self.service.delete(event_id, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertFalse(res)
|
||||
mock_do_request.assert_called_with(
|
||||
f"/v1.0/me/calendar/events/{event_id}",
|
||||
{}, headers={'Authorization': 'Bearer %s' % self.fake_token}, method="DELETE", timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_delete_an_already_cancelled_event(self, mock_do_request):
|
||||
"""
|
||||
When an event has already been cancelled, Outlook may return a status code equals to 403 or 410.
|
||||
In this case, the delete method should return True.
|
||||
"""
|
||||
event_id = 123
|
||||
|
||||
for status in (403, 410):
|
||||
mock_do_request.return_value = (status, "", None)
|
||||
|
||||
res = self.service.delete(event_id, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertTrue(res)
|
||||
mock_do_request.assert_called_with(
|
||||
f"/v1.0/me/calendar/events/{event_id}",
|
||||
{}, headers={'Authorization': 'Bearer %s' % self.fake_token}, method="DELETE", timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_delete_an_existing_event(self, mock_do_request):
|
||||
event_id = 123
|
||||
mock_do_request.return_value = (200, "", None)
|
||||
|
||||
res = self.service.delete(event_id, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertTrue(res)
|
||||
mock_do_request.assert_called_with(
|
||||
f"/v1.0/me/calendar/events/{event_id}",
|
||||
{}, headers={'Authorization': 'Bearer %s' % self.fake_token}, method="DELETE", timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
def test_answer_token_error(self):
|
||||
"""
|
||||
if no token is provided, an exception is raised
|
||||
"""
|
||||
with self.assertRaises(AttributeError):
|
||||
self.service.answer(123, 'ok', {})
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_answer_returns_false_if_event_does_not_exist(self, mock_do_request):
|
||||
event_id = 123
|
||||
answer = "accept"
|
||||
values = {"a": 1, "b": 2}
|
||||
mock_do_request.return_value = (404, "", None)
|
||||
|
||||
res = self.service.answer(event_id, answer, values, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertFalse(res)
|
||||
mock_do_request.assert_called_with(
|
||||
f"/v1.0/me/calendar/events/{event_id}/{answer}",
|
||||
json.dumps(values),
|
||||
self.header, method="POST", timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_answer_to_an_existing_event(self, mock_do_request):
|
||||
event_id = 123
|
||||
answer = "decline"
|
||||
values = {"a": 1, "b": 2}
|
||||
mock_do_request.return_value = (200, "", None)
|
||||
|
||||
res = self.service.answer(event_id, answer, values, token=self.fake_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
self.assertTrue(res)
|
||||
mock_do_request.assert_called_with(
|
||||
f"/v1.0/me/calendar/events/{event_id}/{answer}",
|
||||
json.dumps(values),
|
||||
self.header, method="POST", timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
@patch.object(MicrosoftCalendarService, "_check_full_sync_required")
|
||||
@patch.object(MicrosoftService, "_do_request")
|
||||
def test_get_events_delta_with_outdated_sync_token(self, mock_do_request, mock_check_full_sync_required):
|
||||
""" When an outdated sync token is provided, we must fetch all events again for updating the old token. """
|
||||
# Throw a 'HTTPError' when the token is outdated, thus triggering the fetching of all events.
|
||||
# Simulate a scenario which the full sync is required, such as when getting the 'SyncStateNotFound' error code.
|
||||
mock_do_request.side_effect = [
|
||||
requests.HTTPError(response=MagicMock(status_code=410, error={'code': "SyncStateNotFound"})),
|
||||
self._do_request_result({"value": []}),
|
||||
]
|
||||
mock_check_full_sync_required.return_value = (True)
|
||||
|
||||
# Call the regular 'delta' get events with an outdated token for triggering the all events fetching.
|
||||
self.env.user.microsoft_calendar_sync_token = self.fake_sync_token
|
||||
self.service._get_events_delta(token=self.fake_token, sync_token=self.fake_sync_token, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
# Two calls must have been made: one call with the outdated sync token and another one with no sync token.
|
||||
mock_do_request.assert_has_calls([
|
||||
self.call_with_sync_token,
|
||||
self.call_without_sync_token
|
||||
])
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService, MicrosoftEvent
|
||||
from odoo.exceptions import ValidationError
|
||||
import pytz
|
||||
from datetime import datetime, date
|
||||
from odoo.tests.common import TransactionCase
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
class TestSyncMicrosoft2Odoo(TransactionCase):
|
||||
|
||||
@property
|
||||
def now(self):
|
||||
return pytz.utc.localize(datetime.now()).isoformat()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.recurrence_id = 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA'
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAACyq4xQ=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': '2020-05-06T07:00:00Z', 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAACyq4xQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000D848B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAALKrjF"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAALKrjF"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAALKrjF"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
self.single_event = [
|
||||
{
|
||||
'@odata.type': '#microsoft.graph.event',
|
||||
'@odata.etag': 'W/"AAAAA"',
|
||||
'type': 'singleInstance',
|
||||
'id': "CCCCC",
|
||||
'start': {
|
||||
'dateTime': '2020-05-05T14:30:00.0000000',
|
||||
'timeZone': 'UTC'
|
||||
},
|
||||
'end': {
|
||||
'dateTime': '2020-05-05T16:00:00.0000000',
|
||||
'timeZone': 'UTC'
|
||||
},
|
||||
'location': {
|
||||
'displayName': "a meeting room at Odoo"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
self.datetime_future = pytz.utc.localize(datetime.now() + relativedelta(days=1)).isoformat()
|
||||
|
||||
def sync(self, events):
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(events)
|
||||
|
||||
def test_new_microsoft_recurrence(self):
|
||||
|
||||
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
events = recurrence.calendar_event_ids
|
||||
self.assertTrue(recurrence, "It should have created an recurrence")
|
||||
self.assertEqual(len(events), 3, "It should have created 3 events")
|
||||
self.assertEqual(recurrence.base_event_id, events[0])
|
||||
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event', 'My recurrent event'])
|
||||
self.assertFalse(events[0].allday)
|
||||
self.assertEqual(events[0].start, datetime(2020, 5, 3, 14, 30))
|
||||
self.assertEqual(events[0].stop, datetime(2020, 5, 3, 16, 00))
|
||||
self.assertEqual(events[1].start, datetime(2020, 5, 4, 14, 30))
|
||||
self.assertEqual(events[1].stop, datetime(2020, 5, 4, 16, 00))
|
||||
self.assertEqual(events[2].start, datetime(2020, 5, 5, 14, 30))
|
||||
self.assertEqual(events[2].stop, datetime(2020, 5, 5, 16, 00))
|
||||
|
||||
def test_microsoft_recurrence_delete_one_event(self):
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
|
||||
self.assertTrue(recurrence, "It should keep the recurrence")
|
||||
self.assertEqual(len(events), 2, "It should keep 2 events")
|
||||
self.assertEqual(recurrence.base_event_id, events[0])
|
||||
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event'])
|
||||
|
||||
def test_microsoft_recurrence_change_name_one_event(self):
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ=="', 'createdDateTime': '2020-05-06T08:01:32.4884797Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00807E40504874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event 2', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'originalStart': '2020-05-04T14:30:00Z', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'showAs': 'busy', 'type': 'exception', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
|
||||
self.assertTrue(recurrence, "It should have created an recurrence")
|
||||
self.assertEqual(len(events), 3, "It should have created 3 events")
|
||||
self.assertEqual(recurrence.base_event_id, events[0])
|
||||
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event 2', 'My recurrent event'])
|
||||
|
||||
def test_microsoft_recurrence_change_name_all_event(self):
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event 2', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
|
||||
self.assertTrue(recurrence, "It should keep the recurrence")
|
||||
self.assertEqual(len(events), 3, "It should keep the 3 events")
|
||||
self.assertEqual(recurrence.base_event_id, events[0])
|
||||
self.assertEqual(events.mapped('name'), ['My recurrent event 2', 'My recurrent event 2', 'My recurrent event 2'])
|
||||
|
||||
def test_microsoft_recurrence_change_date_one_event(self):
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZPA=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZPA==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpk8"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZPA=="', 'createdDateTime': '2020-05-06T08:41:52.1067613Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZPA==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00807E40504874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'originalStart': '2020-05-04T14:30:00Z', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'showAs': 'busy', 'type': 'exception', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T17:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpk8"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
|
||||
special_event = self.env['calendar.event'].search([('microsoft_id', '=', 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=')])
|
||||
self.assertTrue(recurrence, "It should have created an recurrence")
|
||||
self.assertTrue(special_event, "It should have created an special event")
|
||||
self.assertEqual(len(events), 3, "It should have created 3 events")
|
||||
self.assertTrue(special_event in events)
|
||||
self.assertEqual(recurrence.base_event_id, events[0])
|
||||
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event', 'My recurrent event'])
|
||||
event_not_special = events - special_event
|
||||
self.assertEqual(event_not_special[0].start, datetime(2020, 5, 3, 14, 30))
|
||||
self.assertEqual(event_not_special[0].stop, datetime(2020, 5, 3, 16, 00))
|
||||
self.assertEqual(event_not_special[1].start, datetime(2020, 5, 5, 14, 30))
|
||||
self.assertEqual(event_not_special[1].stop, datetime(2020, 5, 5, 16, 00))
|
||||
self.assertEqual(special_event.start, datetime(2020, 5, 4, 14, 30))
|
||||
self.assertEqual(special_event.stop, datetime(2020, 5, 4, 17, 00))
|
||||
|
||||
def test_microsoft_recurrence_delete_first_event(self):
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bnw=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bnw==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gf"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gf"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
|
||||
self.assertTrue(recurrence, "It should have created an recurrence")
|
||||
self.assertEqual(len(events), 2, "It should left 2 events")
|
||||
self.assertEqual(recurrence.base_event_id, events[0])
|
||||
self.assertEqual(events[0].start, datetime(2020, 5, 4, 14, 30))
|
||||
self.assertEqual(events[0].stop, datetime(2020, 5, 4, 16, 00))
|
||||
self.assertEqual(events[1].start, datetime(2020, 5, 5, 14, 30))
|
||||
self.assertEqual(events[1].stop, datetime(2020, 5, 5, 16, 00))
|
||||
|
||||
# Now we delete lastest event in Outlook.
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bpg=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bpg==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gm"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
|
||||
self.assertEqual(len(events), 1, "It should have created 1 events")
|
||||
self.assertEqual(recurrence.base_event_id, events[0])
|
||||
|
||||
# Now, we change end datetime of recurrence in Outlook, so all recurrence is recreated (even deleted events)
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bqg=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bqg==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gq"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:30:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gq"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:30:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gq"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:30:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
|
||||
self.assertEqual(len(events), 3, "It should have created 3 events")
|
||||
self.assertEqual(recurrence.base_event_id, events[0])
|
||||
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event', 'My recurrent event'])
|
||||
self.assertEqual(events[0].start, datetime(2020, 5, 3, 14, 30))
|
||||
self.assertEqual(events[0].stop, datetime(2020, 5, 3, 16, 30))
|
||||
self.assertEqual(events[1].start, datetime(2020, 5, 4, 14, 30))
|
||||
self.assertEqual(events[1].stop, datetime(2020, 5, 4, 16, 30))
|
||||
self.assertEqual(events[2].start, datetime(2020, 5, 5, 14, 30))
|
||||
self.assertEqual(events[2].stop, datetime(2020, 5, 5, 16, 30))
|
||||
|
||||
def test_microsoft_recurrence_split_recurrence(self):
|
||||
values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dig=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dig==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-03', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dkw=="', 'createdDateTime': '2020-05-06T13:24:10.0507138Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dkw==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E008000000001A4457A0A923D601000000000000000010000000476AE6084FD718418262DA1AE3E41411', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T17:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-04', 'endDate': '2020-05-06', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8OK"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:30:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8OT"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAADJIEKwAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T17:00:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dkw=="', 'createdDateTime': '2020-05-06T13:25:05.9240043Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dkw==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00807E405051A4457A0A923D601000000000000000010000000476AE6084FD718418262DA1AE3E41411', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event 2', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'originalStart': '2020-05-05T14:30:00Z', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA', 'showAs': 'busy', 'type': 'exception', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAAEA%3D%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAAEA==', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T17:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8OT"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8VBriIAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAAEA==', 'start': {'dateTime': '2020-05-06T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-06T17:00:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
recurrence_1 = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
recurrence_2 = self.env['calendar.recurrence'].search([('microsoft_id', '=', 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA')])
|
||||
|
||||
events_1 = self.env['calendar.event'].search([('recurrence_id', '=', recurrence_1.id)], order='start asc')
|
||||
events_2 = self.env['calendar.event'].search([('recurrence_id', '=', recurrence_2.id)], order='start asc')
|
||||
self.assertTrue(recurrence_1, "It should have created an recurrence")
|
||||
self.assertTrue(recurrence_2, "It should have created an recurrence")
|
||||
self.assertEqual(len(events_1), 1, "It should left 1 event")
|
||||
self.assertEqual(len(events_2), 3, "It should have created 3 events")
|
||||
self.assertEqual(recurrence_1.base_event_id, events_1[0])
|
||||
self.assertEqual(recurrence_2.base_event_id, events_2[0])
|
||||
self.assertEqual(events_1.mapped('name'), ['My recurrent event'])
|
||||
self.assertEqual(events_2.mapped('name'), ['My recurrent event', 'My recurrent event 2', 'My recurrent event'])
|
||||
self.assertEqual(events_1[0].start, datetime(2020, 5, 3, 14, 30))
|
||||
self.assertEqual(events_1[0].stop, datetime(2020, 5, 3, 16, 30))
|
||||
self.assertEqual(events_2[0].start, datetime(2020, 5, 4, 14, 30))
|
||||
self.assertEqual(events_2[0].stop, datetime(2020, 5, 4, 17, 00))
|
||||
self.assertEqual(events_2[1].start, datetime(2020, 5, 5, 14, 30))
|
||||
self.assertEqual(events_2[1].stop, datetime(2020, 5, 5, 17, 00))
|
||||
self.assertEqual(events_2[2].start, datetime(2020, 5, 6, 14, 30))
|
||||
self.assertEqual(events_2[2].stop, datetime(2020, 5, 6, 17, 00))
|
||||
|
||||
def test_microsoft_recurrence_delete(self):
|
||||
recurrence_id = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
event_ids = self.env['calendar.event'].search([('recurrence_id', '=', recurrence_id.id)], order='start asc').ids
|
||||
values = [{'@odata.type': '#microsoft.graph.event', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', '@removed': {'reason': 'deleted'}}]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
|
||||
|
||||
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
|
||||
events = self.env['calendar.event'].browse(event_ids).exists()
|
||||
self.assertFalse(recurrence, "It should remove recurrence")
|
||||
self.assertFalse(events, "It should remove all events")
|
||||
|
||||
def test_attendees_must_have_email(self):
|
||||
"""
|
||||
Synching with a partner without mail raises a ValidationError because Microsoft don't accept attendees without one.
|
||||
"""
|
||||
MicrosoftCal = MicrosoftCalendarService(self.env['microsoft.service'])
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'SuperPartner',
|
||||
})
|
||||
event = self.env['calendar.event'].create({
|
||||
'name': "SuperEvent",
|
||||
'start': datetime(2020, 3, 16, 11, 0),
|
||||
'stop': datetime(2020, 3, 16, 13, 0),
|
||||
'partner_ids': [(4, partner.id)],
|
||||
})
|
||||
with self.assertRaises(ValidationError):
|
||||
event._sync_odoo2microsoft(MicrosoftCal)
|
||||
|
||||
def test_cancel_occurence_of_recurrent_event(self):
|
||||
""" The user is invited to a recurrent event. When synced, all events are present, there are three occurrences:
|
||||
- 07/15/2021, 15:00-15:30
|
||||
- 07/16/2021, 15:00-15:30
|
||||
- 07/17/2021, 15:00-15:30
|
||||
Then, the organizer cancels the second occurrence -> The latter should not be displayed anymore
|
||||
"""
|
||||
microsoft_id = 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgBGAAADZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAA='
|
||||
# self.env.user.partner_id.email = "odoo_bf_user01@outlook.com"
|
||||
first_sync_values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"pynKRnkCyUmnqILQHcLZEQAABElcNQ=="', 'createdDateTime': '2021-07-15T14:47:40.2996962Z', 'lastModifiedDateTime': '2021-07-15T14:47:40.3783507Z', 'changeKey': 'pynKRnkCyUmnqILQHcLZEQAABElcNQ==', 'categories': [], 'transactionId': None, 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000B35B3B5A8879D70100000000000000001000000008A0949F4EC0A1479E4ED178D87EF679', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'Recurrent Event 1646', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': False, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'tentative', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgBGAAADZ59RIxdyh0Kt%2FMXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAA%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'allowNewTimeProposals': True, 'OccurrenceId': None, 'isDraft': False, 'hideAttendees': False, 'CalendarEventClassifications': [], 'AutoRoomBookingOptions': None, 'onlineMeeting': None, 'id': microsoft_id, 'responseStatus': {'response': 'notResponded', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2021-07-15T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-15T15:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2021-07-15', 'endDate': '2021-07-17', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [{'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}, {'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo01 Outlook01', 'address': 'odoo_bf_user01@outlook.com'}}], 'organizer': {'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVw1"', 'seriesMasterId': ('%s' % microsoft_id), 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlHI305wABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-15T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-15T15:30:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVw1"', 'seriesMasterId': microsoft_id, 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlH7KejgABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-16T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-16T15:30:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVw1"', 'seriesMasterId': microsoft_id, 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlItdINQABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-17T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-17T15:30:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
second_sync_values = [
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"pynKRnkCyUmnqILQHcLZEQAABElcUw=="', 'createdDateTime': '2021-07-15T14:47:40.2996962Z', 'lastModifiedDateTime': '2021-07-15T14:51:25.2560888Z', 'changeKey': 'pynKRnkCyUmnqILQHcLZEQAABElcUw==', 'categories': [], 'transactionId': None, 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000B35B3B5A8879D70100000000000000001000000008A0949F4EC0A1479E4ED178D87EF679', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'Recurrent Event 1646', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': False, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'tentative', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgBGAAADZ59RIxdyh0Kt%2FMXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAA%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'allowNewTimeProposals': True, 'OccurrenceId': None, 'isDraft': False, 'hideAttendees': False, 'CalendarEventClassifications': [], 'id': microsoft_id, 'responseStatus': {'response': 'notResponded', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2021-07-15T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-15T15:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2021-07-15', 'endDate': '2021-07-17', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [{'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}, {'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo01 Outlook01', 'address': 'odoo_bf_user01@outlook.com'}}], 'organizer': {'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVxT"', 'seriesMasterId': microsoft_id, 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlHI305wABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-15T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-15T15:30:00.0000000', 'timeZone': 'UTC'}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"pynKRnkCyUmnqILQHcLZEQAABElcUw=="', 'createdDateTime': '2021-07-15T14:51:25.1366139Z', 'lastModifiedDateTime': '2021-07-15T14:51:25.136614Z', 'changeKey': 'pynKRnkCyUmnqILQHcLZEQAABElcUw==', 'categories': [], 'transactionId': None, 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00807E50710B35B3B5A8879D70100000000000000001000000008A0949F4EC0A1479E4ED178D87EF679', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'Canceled: Recurrent Event 1646', 'bodyPreview': '', 'importance': 'high', 'sensitivity': 'normal', 'originalStart': '2021-07-16T15:00:00Z', 'isAllDay': False, 'isCancelled': True, 'isOrganizer': False, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': microsoft_id, 'showAs': 'free', 'type': 'exception', 'webLink': 'https://outlook.live.com/owa/?itemid=AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlH7KejgABGAAACZ59RIxdyh0Kt%2FMXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'allowNewTimeProposals': True, 'OccurrenceId': ('OID.%s.2021-07-16' % microsoft_id), 'isDraft': False, 'hideAttendees': False, 'CalendarEventClassifications': [], 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlH7KejgABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'responseStatus': {'response': 'notResponded', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8">\r\n<meta name="Generator" content="Microsoft Exchange Server">\r\n<!-- converted from text -->\r\n<style><!-- .EmailQuote { margin-left: 1pt; padding-left: 4pt; border-left: #800000 2px solid; } --></style></head>\r\n<body>\r\n<font size="2"><span style="font-size:11pt;"><div class="PlainText"> </div></span></font>\r\n</body>\r\n</html>\r\n'}, 'start': {'dateTime': '2021-07-16T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-16T15:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'attendees': [{'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}, {'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo01 Outlook01', 'address': 'odoo_bf_user01@outlook.com'}}], 'organizer': {'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}},
|
||||
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVxT"', 'seriesMasterId': microsoft_id, 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlItdINQABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-17T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-17T15:30:00.0000000', 'timeZone': 'UTC'}}
|
||||
]
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(first_sync_values))
|
||||
recurrent_event = self.env['calendar.recurrence'].search([('microsoft_id', '=', 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgBGAAADZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAA=')])
|
||||
self.assertEqual(len(recurrent_event.calendar_event_ids), 3)
|
||||
|
||||
# Need to cheat on the write date, otherwise the second sync won't update the events
|
||||
recurrent_event.write_date = datetime(2021, 7, 15, 14, 00)
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(second_sync_values))
|
||||
self.assertEqual(len(recurrent_event.calendar_event_ids), 2)
|
||||
|
||||
events = recurrent_event.calendar_event_ids.sorted(key=lambda e: e.start)
|
||||
self.assertEqual(events[0].start, datetime(2021, 7, 15, 15, 00))
|
||||
self.assertEqual(events[0].stop, datetime(2021, 7, 15, 15, 30))
|
||||
self.assertEqual(events[1].start, datetime(2021, 7, 17, 15, 00))
|
||||
self.assertEqual(events[1].stop, datetime(2021, 7, 17, 15, 30))
|
||||
|
||||
def test_use_classic_location(self):
|
||||
ms_event = self.single_event
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
|
||||
|
||||
event = self.env['calendar.event'].search([("microsoft_id", "=", ms_event[0]["id"])])
|
||||
self.assertEqual(event.location, ms_event[0]["location"]["displayName"])
|
||||
|
||||
def test_use_url_location(self):
|
||||
ms_event = self.single_event
|
||||
ms_event[0]["location"]["displayName"] = "https://mylocation.com/meeting-room"
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
|
||||
|
||||
event = self.env['calendar.event'].search([("microsoft_id", "=", ms_event[0]["id"])])
|
||||
self.assertEqual(event.location, ms_event[0]["location"]["displayName"])
|
||||
|
||||
def test_use_specific_virtual_location(self):
|
||||
"""
|
||||
If the location of the Outlook event is a specific virtual location (such as a video Teams meeting),
|
||||
use it as videocall location.
|
||||
"""
|
||||
ms_event = self.single_event
|
||||
ms_event[0]["location"]["displayName"] = "https://teams.microsoft.com/l/meeting/1234"
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
|
||||
|
||||
event = self.env['calendar.event'].search([("microsoft_id", "=", ms_event[0]["id"])])
|
||||
self.assertEqual(event.location, False)
|
||||
self.assertEqual(event.videocall_location, ms_event[0]["location"]["displayName"])
|
||||
|
||||
def test_outlook_event_has_online_meeting_url(self):
|
||||
ms_event = self.single_event
|
||||
ms_event[0].update({
|
||||
'isOnlineMeeting': True,
|
||||
'onlineMeeting': {'joinUrl': 'https://video-meeting.com/1234'}
|
||||
})
|
||||
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
|
||||
|
||||
event = self.env['calendar.event'].search([("microsoft_id", "=", ms_event[0]["id"])])
|
||||
self.assertEqual(event.videocall_location, ms_event[0]["onlineMeeting"]["joinUrl"])
|
||||
|
||||
def test_event_reminder_emails_with_microsoft_id(self):
|
||||
"""
|
||||
Odoo shouldn't send email reminders for synced events.
|
||||
Test that events synced to Microsoft (with a `microsoft_id`)
|
||||
are excluded from email alarm notifications.
|
||||
"""
|
||||
now = datetime.now()
|
||||
start = now - relativedelta(minutes=30)
|
||||
end = now + relativedelta(hours=2)
|
||||
alarm = self.env['calendar.alarm'].create({
|
||||
'name': 'Alarm',
|
||||
'alarm_type': 'email',
|
||||
'interval': 'minutes',
|
||||
'duration': 30,
|
||||
})
|
||||
ms_event = self.single_event
|
||||
ms_event[0].update({
|
||||
'isOnlineMeeting': True,
|
||||
'alarm_id': alarm.id,
|
||||
'start': {
|
||||
'dateTime': pytz.utc.localize(start).isoformat(),
|
||||
'timeZone': 'Europe/Brussels'
|
||||
},
|
||||
'reminders': {'overrides': [{"method": "email", "minutes": 30}], 'useDefault': False},
|
||||
'end': {
|
||||
'dateTime': pytz.utc.localize(end).isoformat(),
|
||||
'timeZone': 'Europe/Brussels'
|
||||
},
|
||||
})
|
||||
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
|
||||
events_by_alarm = self.env['calendar.alarm_manager']._get_events_by_alarm_to_notify('email')
|
||||
self.assertFalse(events_by_alarm, "Events with microsoft_id should not trigger reminders")
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
|
||||
from odoo.addons.microsoft_calendar.models.res_users import User
|
||||
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftSync
|
||||
from odoo.modules.registry import Registry
|
||||
from odoo.addons.microsoft_account.models.microsoft_service import TIMEOUT
|
||||
|
||||
|
||||
def patch_api(func):
|
||||
@patch.object(MicrosoftSync, '_microsoft_insert', MagicMock())
|
||||
@patch.object(MicrosoftSync, '_microsoft_delete', MagicMock())
|
||||
@patch.object(MicrosoftSync, '_microsoft_patch', MagicMock())
|
||||
def patched(self, *args, **kwargs):
|
||||
return func(self, *args, **kwargs)
|
||||
return patched
|
||||
|
||||
@patch.object(User, '_get_microsoft_calendar_token', lambda user: 'dummy-token')
|
||||
class TestSyncOdoo2Microsoft(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
|
||||
|
||||
def assertMicrosoftEventInserted(self, values):
|
||||
MicrosoftSync._microsoft_insert.assert_called_once_with(self.microsoft_service, values)
|
||||
|
||||
def assertMicrosoftEventNotInserted(self):
|
||||
MicrosoftSync._microsoft_insert.assert_not_called()
|
||||
|
||||
def assertMicrosoftEventPatched(self, microsoft_id, values, timeout=None):
|
||||
expected_args = (microsoft_id, values)
|
||||
expected_kwargs = {'timeout': timeout} if timeout else {}
|
||||
MicrosoftSync._microsoft_patch.assert_called_once()
|
||||
args, kwargs = MicrosoftSync._microsoft_patch.call_args
|
||||
self.assertEqual(args[1:], expected_args) # skip Google service arg
|
||||
self.assertEqual(kwargs, expected_kwargs)
|
||||
|
||||
@patch_api
|
||||
def test_stop_synchronization(self):
|
||||
self.env.user.stop_microsoft_synchronization()
|
||||
self.assertTrue(self.env.user.microsoft_synchronization_stopped, "The microsoft synchronization flag should be switched on")
|
||||
self.assertFalse(self.env.user._sync_microsoft_calendar(self.microsoft_service), "The microsoft synchronization should be stopped")
|
||||
year = date.today().year - 1
|
||||
|
||||
# If synchronization stopped, creating a new event should not call _google_insert.
|
||||
self.env['calendar.event'].create({
|
||||
'name': "Event",
|
||||
'start': datetime(year, 1, 15, 8, 0),
|
||||
'stop': datetime(year, 1, 15, 18, 0),
|
||||
'privacy': 'private',
|
||||
})
|
||||
self.assertMicrosoftEventNotInserted()
|
||||
|
||||
@patch_api
|
||||
def test_restart_synchronization(self):
|
||||
# Test new event created after stopping synchronization are correctly patched when restarting sync.
|
||||
self.maxDiff = None
|
||||
microsoft_id = 'aaaaaaaaa'
|
||||
year = date.today().year
|
||||
partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
|
||||
user = self.env['res.users'].create({
|
||||
'name': 'Test user Calendar',
|
||||
'login': 'jean-luc@opoo.com',
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
user.stop_microsoft_synchronization()
|
||||
# In case of full sync, limit to a range of 1y in past and 1y in the future by default
|
||||
event = self.env['calendar.event'].with_user(user).create({
|
||||
'microsoft_id': microsoft_id,
|
||||
'name': "Event",
|
||||
'start': datetime(year, 1, 15, 8, 0),
|
||||
'stop': datetime(year, 1, 15, 18, 0),
|
||||
'partner_ids': [(4, partner.id)],
|
||||
})
|
||||
|
||||
user.with_user(user).restart_microsoft_synchronization()
|
||||
event.with_user(user)._sync_odoo2microsoft(self.microsoft_service)
|
||||
microsoft_guid = self.env['ir.config_parameter'].sudo().get_param('microsoft_calendar.microsoft_guid', False)
|
||||
self.assertMicrosoftEventPatched(event.microsoft_id, {
|
||||
'id': event.microsoft_id,
|
||||
'start': {'dateTime': '%s-01-15T08:00:00+00:00' % year, 'timeZone': 'Europe/London'},
|
||||
'end': {'dateTime': '%s-01-15T18:00:00+00:00' % year, 'timeZone': 'Europe/London'},
|
||||
'subject': 'Event',
|
||||
'body': {'content': '', 'contentType': 'html'},
|
||||
'attendees': [],
|
||||
'isAllDay': False,
|
||||
'isOrganizer': True,
|
||||
'isReminderOn': False,
|
||||
'sensitivity': 'normal',
|
||||
'showAs': 'busy',
|
||||
'location': {'displayName': ''},
|
||||
'organizer': {'emailAddress': {'address': 'jean-luc@opoo.com', 'name': 'Test user Calendar'}},
|
||||
'reminderMinutesBeforeStart': 0,
|
||||
'singleValueExtendedProperties': [{
|
||||
'id': 'String {%s} Name odoo_id' % microsoft_guid,
|
||||
'value': str(event.id),
|
||||
}, {
|
||||
'id': 'String {%s} Name owner_odoo_id' % microsoft_guid,
|
||||
'value': str(user.id),
|
||||
}
|
||||
]
|
||||
})
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue