summaryrefslogtreecommitdiffstats
path: root/comm/calendar/test/browser/invitations/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/test/browser/invitations/head.js')
-rw-r--r--comm/calendar/test/browser/invitations/head.js942
1 files changed, 942 insertions, 0 deletions
diff --git a/comm/calendar/test/browser/invitations/head.js b/comm/calendar/test/browser/invitations/head.js
new file mode 100644
index 0000000000..24835c3021
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/head.js
@@ -0,0 +1,942 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Common functions for the imip-bar tests.
+ *
+ * Note that these tests are heavily tied to the .eml files found in the data
+ * folder.
+ */
+
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { CalItipDefaultEmailTransport } = ChromeUtils.import(
+ "resource:///modules/CalItipEmailTransport.jsm"
+);
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { FileTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+registerCleanupFunction(async () => {
+ // Some tests that open new windows don't return focus to the main window
+ // in a way that satisfies mochitest, and the test times out.
+ Services.focus.focusedWindow = window;
+ document.body.focus();
+});
+
+class EmailTransport extends CalItipDefaultEmailTransport {
+ sentItems = [];
+
+ sentMsgs = [];
+
+ getMsgSend() {
+ let { sentMsgs } = this;
+ return {
+ sendMessageFile(
+ userIdentity,
+ accountKey,
+ composeFields,
+ messageFile,
+ deleteSendFileOnCompletion,
+ digest,
+ deliverMode,
+ msgToReplace,
+ listener,
+ statusFeedback,
+ smtpPassword
+ ) {
+ sentMsgs.push({
+ userIdentity,
+ accountKey,
+ composeFields,
+ messageFile,
+ deleteSendFileOnCompletion,
+ digest,
+ deliverMode,
+ msgToReplace,
+ listener,
+ statusFeedback,
+ smtpPassword,
+ });
+ },
+ };
+ }
+
+ sendItems(recipients, itipItem, fromAttendee) {
+ this.sentItems.push({ recipients, itipItem, fromAttendee });
+ return super.sendItems(recipients, itipItem, fromAttendee);
+ }
+
+ reset() {
+ this.sentItems = [];
+ this.sentMsgs = [];
+ }
+}
+
+async function openMessageFromFile(file) {
+ let fileURL = Services.io
+ .newFileURI(file)
+ .mutate()
+ .setQuery("type=application/x-message-display")
+ .finalize();
+
+ let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ window.openDialog(
+ "chrome://messenger/content/messageWindow.xhtml",
+ "_blank",
+ "all,chrome,dialog=no,status,toolbar",
+ fileURL
+ );
+ let win = await winPromise;
+ await BrowserTestUtils.waitForEvent(win, "MsgLoaded");
+ await TestUtils.waitForCondition(() => Services.focus.activeWindow == win);
+ return win;
+}
+
+/**
+ * Opens an iMIP message file and waits for the imip-bar to appear.
+ *
+ * @param {nsIFile} file
+ * @returns {Window}
+ */
+async function openImipMessage(file) {
+ let win = await openMessageFromFile(file);
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let imipBar = aboutMessage.document.getElementById("imip-bar");
+ await TestUtils.waitForCondition(() => !imipBar.collapsed, "imip-bar shown");
+
+ if (Services.prefs.getBoolPref("calendar.itip.newInvitationDisplay")) {
+ // CalInvitationDisplay.show() does some async activities before the panel is added.
+ await TestUtils.waitForCondition(
+ () =>
+ win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel"),
+ "calendar-invitation-panel shown"
+ );
+ }
+ return win;
+}
+
+/**
+ * Clicks on one of the imip-bar action buttons.
+ *
+ * @param {Window} win
+ * @param {string} id
+ */
+async function clickAction(win, id) {
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let action = aboutMessage.document.getElementById(id);
+ await TestUtils.waitForCondition(() => !action.hidden, `button "#${id}" shown`);
+
+ EventUtils.synthesizeMouseAtCenter(action, {}, aboutMessage);
+ await TestUtils.waitForCondition(() => action.hidden, `button "#${id}" hidden`);
+}
+
+/**
+ * Clicks on one of the imip-bar actions from a dropdown menu.
+ *
+ * @param {Window} win The window the imip message is opened in.
+ * @param {string} buttonId The id of the <toolbarbutton> containing the menu.
+ * @param {string} actionId The id of the menu item to click.
+ */
+async function clickMenuAction(win, buttonId, actionId) {
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let actionButton = aboutMessage.document.getElementById(buttonId);
+ await TestUtils.waitForCondition(() => !actionButton.hidden, `"${buttonId}" shown`);
+
+ let actionMenu = actionButton.querySelector("menupopup");
+ let menuShown = BrowserTestUtils.waitForEvent(actionMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(actionButton.querySelector("dropmarker"), {}, aboutMessage);
+ await menuShown;
+ actionMenu.activateItem(aboutMessage.document.getElementById(actionId));
+ await TestUtils.waitForCondition(() => actionButton.hidden, `action menu "#${buttonId}" hidden`);
+}
+
+const unpromotedProps = ["location", "description", "sequence", "x-moz-received-dtstamp"];
+
+/**
+ * An object where the keys are paths/selectors and the values are the values
+ * we expect to encounter.
+ *
+ * @typedef {object} Comparable
+ */
+
+/**
+ * Compares the paths specified in the expected object against the provided
+ * actual object.
+ *
+ * @param {object} actual This is expected to be a calIEvent or calIAttendee but
+ * can also be an array of both etc.
+ * @param {Comparable} expected
+ */
+function compareProperties(actual, expected, prefix = "") {
+ Assert.equal(typeof actual, "object", `${prefix || "provided value"} is an object`);
+ for (let [key, value] of Object.entries(expected)) {
+ if (key.includes(".")) {
+ let keys = key.split(".");
+ let head = keys[0];
+ let tail = keys.slice(1).join(".");
+ compareProperties(actual[head], { [tail]: value }, [prefix, head].filter(k => k).join("."));
+ continue;
+ }
+
+ let path = [prefix, key].filter(k => k).join(".");
+ let actualValue = unpromotedProps.includes(key) ? actual.getProperty(key) : actual[key];
+ Assert.equal(actualValue, value, `property "${path}" is "${value}"`);
+ }
+}
+
+/**
+ * Compares the text contents of the selectors specified on the inviatation panel
+ * to the expected value for each.
+ *
+ * @param {ShadowRoot} root The invitation panel's ShadowRoot instance.
+ * @param {Comparable} expected
+ */
+function compareShownPanelValues(root, expected) {
+ for (let [key, value] of Object.entries(expected)) {
+ value = Array.isArray(value) ? value.join("") : value;
+ Assert.equal(
+ root.querySelector(key).textContent.trim(),
+ value,
+ `property "${key}" is "${value}"`
+ );
+ }
+}
+
+/**
+ * Clicks on one of the invitation panel action buttons.
+ *
+ * @param {Window} panel
+ * @param {string} id
+ * @param {boolean} sendResponse
+ */
+async function clickPanelAction(panel, id, sendResponse = true) {
+ let promise = BrowserTestUtils.promiseAlertDialogOpen(sendResponse ? "accept" : "cancel");
+ let button = panel.shadowRoot.getElementById(id);
+ EventUtils.synthesizeMouseAtCenter(button, {}, panel.ownerGlobal);
+ await promise;
+ await BrowserTestUtils.waitForEvent(panel.ownerGlobal, "onItipItemActionFinished");
+}
+
+/**
+ * Tests that an attempt to reply to the organizer of the event with the correct
+ * details occurred.
+ *
+ * @param {EmailTransport} transport
+ * @param {nsIdentity} identity
+ * @param {string} partStat
+ */
+async function doReplyTest(transport, identity, partStat) {
+ info("Verifying the attempt to send a response uses the correct data");
+ Assert.equal(transport.sentItems.length, 1, "itip subsystem attempted to send a response");
+ compareProperties(transport.sentItems[0], {
+ "recipients.0.id": "mailto:sender@example.com",
+ "itipItem.responseMethod": "REPLY",
+ "fromAttendee.id": "mailto:receiver@example.com",
+ "fromAttendee.participationStatus": partStat,
+ });
+
+ // The itipItem is used to generate the iTIP data in the message body.
+ info("Verifying the reply calItipItem attendee list");
+ let replyItem = transport.sentItems[0].itipItem.getItemList()[0];
+ let replyAttendees = replyItem.getAttendees();
+ Assert.equal(replyAttendees.length, 1, "reply has one attendee");
+ compareProperties(replyAttendees[0], {
+ id: "mailto:receiver@example.com",
+ participationStatus: partStat,
+ });
+
+ info("Verifying the call to the message subsystem");
+ Assert.equal(transport.sentMsgs.length, 1, "transport sent 1 message");
+ compareProperties(transport.sentMsgs[0], {
+ userIdentity: identity,
+ "composeFields.from": "receiver@example.com",
+ "composeFields.to": "Sender <sender@example.com>",
+ });
+ Assert.ok(transport.sentMsgs[0].messageFile.exists(), "message file was created");
+}
+
+/**
+ * @typedef {object} ImipBarActionTestConf
+ *
+ * @property {calICalendar} calendar The calendar used for the test.
+ * @property {calIItipTranport} transport The transport used for the test.
+ * @property {nsIIdentity} identity The identity expected to be used to
+ * send the reply.
+ * @property {boolean} isRecurring Indicates whether to treat the event as a
+ * recurring event or not.
+ * @property {string} partStat The participationStatus of the receiving user to
+ * expect.
+ * @property {boolean} noReply If true, do not expect an attempt to send a reply.
+ * @property {boolean} noSend If true, expect the reply attempt to stop after the
+ * user is prompted.
+ * @property {boolean} isMajor For update tests indicates if the changes expected
+ * are major or minor.
+ */
+
+/**
+ * Test the properties of an event created from the imip-bar and optionally, the
+ * attempt to send a reply.
+ *
+ * @param {ImipBarActionTestConf} conf
+ * @param {calIEvent|calIEvent[]} item
+ */
+async function doImipBarActionTest(conf, event) {
+ let { calendar, transport, identity, partStat, isRecurring, noReply, noSend } = conf;
+ let events = [event];
+ let startDates = ["20220316T110000Z"];
+ let endDates = ["20220316T113000Z"];
+
+ if (isRecurring) {
+ startDates = [...startDates, "20220317T110000Z", "20220318T110000Z"];
+ endDates = [...endDates, "20220317T113000Z", "20220318T113000Z"];
+ events = event.parentItem.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(events.length, 3, "reccurring event has 3 occurrences");
+ }
+
+ info("Verifying relevant properties of each event occurrence");
+ for (let [index, occurrence] of events.entries()) {
+ compareProperties(occurrence, {
+ id: "02e79b96",
+ title: isRecurring ? "Repeat Event" : "Single Event",
+ "calendar.name": calendar.name,
+ ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}),
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ description: "An event invitation.",
+ location: "Somewhere",
+ sequence: "0",
+ "x-moz-received-dtstamp": "20220316T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ // Alarms should be ignored.
+ Assert.equal(
+ occurrence.getAlarms().length,
+ 0,
+ `${isRecurring ? "occurrence" : "event"} has no reminders`
+ );
+
+ info("Verifying attendee list and participation status");
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.participationStatus": partStat,
+ "1.id": "mailto:receiver@example.com",
+ "2.id": "mailto:other@example.com",
+ "2.participationStatus": "NEEDS-ACTION",
+ });
+ }
+
+ if (noReply) {
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ }
+ if (noReply || noSend) {
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ return;
+ }
+ await doReplyTest(transport, identity, partStat);
+}
+
+/**
+ * Tests the recognition and application of a minor update to an existing event.
+ * An update is considered minor if the SEQUENCE property has not changed but
+ * the DTSTAMP has.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doMinorUpdateTest(conf) {
+ let { transport, calendar, partStat, isRecurring } = conf;
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ let prevEventIcs = event.icalString;
+
+ transport.reset();
+
+ let updatePath = isRecurring ? "data/repeat-update-minor.eml" : "data/update-minor.eml";
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath(updatePath)));
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let updateButton = aboutMessage.document.getElementById("imipUpdateButton");
+ Assert.ok(!updateButton.hidden, `#${updateButton.id} button shown`);
+ EventUtils.synthesizeMouseAtCenter(updateButton, {}, aboutMessage);
+
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ return event.icalString != prevEventIcs;
+ }, "event updated");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ let events = [event];
+ let startDates = ["20220316T110000Z"];
+ let endDates = ["20220316T113000Z"];
+ if (isRecurring) {
+ startDates = [...startDates, "20220317T110000Z", "20220318T110000Z"];
+ endDates = [...endDates, "20220317T113000Z", "20220318T113000Z"];
+ events = event.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(events.length, 3, "reccurring event has 3 occurrences");
+ }
+
+ info("Verifying relevant properties of each event occurrence");
+ for (let [index, occurrence] of events.entries()) {
+ compareProperties(occurrence, {
+ id: "02e79b96",
+ title: "Updated Event",
+ "calendar.name": calendar.name,
+ ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}),
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ description: "Updated description.",
+ location: "Updated location",
+ sequence: "0",
+ "x-moz-received-dtstamp": "20220318T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ // Note: It seems we do not keep the order of the attendees list for updates.
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:other@example.com",
+ "1.participationStatus": "NEEDS-ACTION",
+ "2.participationStatus": partStat,
+ "2.id": "mailto:receiver@example.com",
+ });
+ }
+
+ Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response");
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ await calendar.deleteItem(event);
+}
+
+const actionIds = {
+ single: {
+ button: {
+ ACCEPTED: "imipAcceptButton",
+ TENTATIVE: "imipTentativeButton",
+ DECLINED: "imipDeclineButton",
+ },
+ noReply: {
+ ACCEPTED: "imipAcceptButton_AcceptDontSend",
+ TENTATIVE: "imipTentativeButton_TentativeDontSend",
+ DECLINED: "imipDeclineButton_DeclineDontSend",
+ },
+ },
+ recurring: {
+ button: {
+ ACCEPTED: "imipAcceptRecurrencesButton",
+ TENTATIVE: "imipTentativeRecurrencesButton",
+ DECLINED: "imipDeclineRecurrencesButton",
+ },
+ noReply: {
+ ACCEPTED: "imipAcceptRecurrencesButton_AcceptDontSend",
+ TENTATIVE: "imipTentativeRecurrencesButton_TentativeDontSend",
+ DECLINED: "imipDeclineRecurrencesButton_DeclineDontSend",
+ },
+ },
+};
+
+/**
+ * Tests the recognition and application of a major update to an existing event.
+ * An update is considered major if the SEQUENCE property has changed. For major
+ * updates, the imip-bar prompts the user to re-confirm their attendance.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doMajorUpdateTest(conf) {
+ let { transport, identity, calendar, partStat, isRecurring, noReply } = conf;
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ let prevEventIcs = event.icalString;
+
+ transport.reset();
+
+ let updatePath = isRecurring ? "data/repeat-update-major.eml" : "data/update-major.eml";
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath(updatePath)));
+ let actions = isRecurring ? actionIds.recurring : actionIds.single;
+ if (noReply) {
+ let { button, noReply } = actions;
+ await clickMenuAction(win, button[partStat], noReply[partStat]);
+ } else {
+ await clickAction(win, actions.button[partStat]);
+ }
+
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ return event.icalString != prevEventIcs;
+ }, "event updated");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ if (noReply) {
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ } else {
+ await doReplyTest(transport, identity, partStat);
+ }
+
+ let events = [event];
+ let startDates = ["20220316T050000Z"];
+ let endDates = ["20220316T053000Z"];
+ if (isRecurring) {
+ startDates = [...startDates, "20220317T050000Z", "20220318T050000Z"];
+ endDates = [...endDates, "20220317T053000Z", "20220318T053000Z"];
+ events = event.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(events.length, 3, "reccurring event has 3 occurrences");
+ }
+
+ for (let [index, occurrence] of events.entries()) {
+ compareProperties(occurrence, {
+ id: "02e79b96",
+ title: isRecurring ? "Repeat Event" : "Single Event",
+ "calendar.name": calendar.name,
+ ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}),
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ description: "An event invitation.",
+ location: "Somewhere",
+ sequence: "2",
+ "x-moz-received-dtstamp": "20220316T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:other@example.com",
+ "1.participationStatus": "NEEDS-ACTION",
+ "2.participationStatus": partStat,
+ "2.id": "mailto:receiver@example.com",
+ });
+ }
+ await calendar.deleteItem(event);
+}
+
+/**
+ * Tests the recognition and application of a minor update exception to an
+ * existing recurring event.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doMinorExceptionTest(conf) {
+ let { transport, calendar, partStat } = conf;
+ let recurrenceId = cal.createDateTime("20220317T110000Z");
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ let originalProps = {
+ id: "02e79b96",
+ "recurrenceId.icalString": "20220317T110000Z",
+ title: event.title,
+ "calendar.name": calendar.name,
+ "startDate.icalString": event.startDate.icalString,
+ "endDate.icalString": event.endDate.icalString,
+ description: event.getProperty("DESCRIPTION"),
+ location: event.getProperty("LOCATION"),
+ sequence: "0",
+ "x-moz-received-dtstamp": event.getProperty("x-moz-received-dtstamp"),
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ };
+
+ Assert.ok(
+ !event.recurrenceInfo.getExceptionFor(recurrenceId),
+ `no exception exists for ${recurrenceId}`
+ );
+
+ transport.reset();
+
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml")));
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let updateButton = aboutMessage.document.getElementById("imipUpdateButton");
+ Assert.ok(!updateButton.hidden, `#${updateButton.id} button shown`);
+ EventUtils.synthesizeMouseAtCenter(updateButton, {}, aboutMessage);
+
+ let exception;
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ exception = event.recurrenceInfo.getExceptionFor(recurrenceId);
+ return exception;
+ }, "event exception applied");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response");
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+
+ info("Verifying relevant properties of the exception");
+ compareProperties(exception, {
+ id: "02e79b96",
+ "recurrenceId.icalString": "20220317T110000Z",
+ title: "Exception title",
+ "calendar.name": calendar.name,
+ "startDate.icalString": "20220317T110000Z",
+ "endDate.icalString": "20220317T113000Z",
+ description: "Exception description",
+ location: "Exception location",
+ sequence: "0",
+ "x-moz-received-dtstamp": "20220318T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ compareProperties(exception.getAttendees(), {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:other@example.com",
+ "1.participationStatus": "NEEDS-ACTION",
+ "2.id": "mailto:receiver@example.com",
+ "2.participationStatus": partStat,
+ });
+
+ let occurrences = event.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(occurrences.length, 3, "reccurring event still has 3 occurrences");
+
+ info("Verifying relevant properties of the other occurrences");
+
+ let startDates = ["20220316T110000Z", "20220317T110000Z", "20220318T110000Z"];
+ let endDates = ["20220316T113000Z", "20220317T113000Z", "20220318T113000Z"];
+ for (let [index, occurrence] of occurrences.entries()) {
+ if (occurrence.startDate.compare(recurrenceId) == 0) {
+ continue;
+ }
+ compareProperties(occurrence, {
+ ...originalProps,
+ "recurrenceId.icalString": startDates[index],
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ });
+
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:receiver@example.com",
+ "1.participationStatus": partStat,
+ "2.id": "mailto:other@example.com",
+ "2.participationStatus": "NEEDS-ACTION",
+ });
+ }
+
+ await calendar.deleteItem(event);
+}
+
+/**
+ * Tests the recognition and application of a major update exception to an
+ * existing recurring event.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doMajorExceptionTest(conf) {
+ let { transport, identity, calendar, partStat, noReply } = conf;
+ let recurrenceId = cal.createDateTime("20220317T110000Z");
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ let originalProps = {
+ id: "02e79b96",
+ "recurrenceId.icalString": "20220317T110000Z",
+ title: event.title,
+ "calendar.name": calendar.name,
+ "startDate.icalString": event.startDate.icalString,
+ "endDate.icalString": event.endDate.icalString,
+ description: event.getProperty("DESCRIPTION"),
+ location: event.getProperty("LOCATION"),
+ sequence: "0",
+ "x-moz-received-dtstamp": event.getProperty("x-moz-received-dtstamp"),
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ };
+ let originalPartStat = event
+ .getAttendees()
+ .find(att => att.id == "mailto:receiver@example.com").participationStatus;
+
+ Assert.ok(
+ !event.recurrenceInfo.getExceptionFor(recurrenceId),
+ `no exception exists for ${recurrenceId}`
+ );
+
+ transport.reset();
+
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml")));
+ if (noReply) {
+ let { button, noReply } = actionIds.single;
+ await clickMenuAction(win, button[partStat], noReply[partStat]);
+ } else {
+ await clickAction(win, actionIds.single.button[partStat]);
+ }
+
+ let exception;
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ exception = event.recurrenceInfo.getExceptionFor(recurrenceId);
+ return exception;
+ }, "event exception applied");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ if (noReply) {
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ } else {
+ await doReplyTest(transport, identity, partStat);
+ }
+
+ info("Verifying relevant properties of the exception");
+
+ compareProperties(exception, {
+ ...originalProps,
+ "startDate.icalString": "20220317T050000Z",
+ "endDate.icalString": "20220317T053000Z",
+ sequence: "2",
+ });
+
+ compareProperties(exception.getAttendees(), {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:other@example.com",
+ "1.participationStatus": "NEEDS-ACTION",
+ "2.id": "mailto:receiver@example.com",
+ "2.participationStatus": partStat,
+ });
+
+ let occurrences = event.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(occurrences.length, 3, "reccurring event still has 3 occurrences");
+
+ info("Verifying relevant properties of the other occurrences");
+
+ let startDates = ["20220316T110000Z", "20220317T110000Z", "20220318T110000Z"];
+ let endDates = ["20220316T113000Z", "20220317T113000Z", "20220318T113000Z"];
+ for (let [index, occurrence] of occurrences.entries()) {
+ if (occurrence.startDate.icalString == "20220317T050000Z") {
+ continue;
+ }
+ compareProperties(occurrence, {
+ ...originalProps,
+ "recurrenceId.icalString": startDates[index],
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ });
+
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:receiver@example.com",
+ "1.participationStatus": originalPartStat,
+ "2.id": "mailto:other@example.com",
+ "2.participationStatus": "NEEDS-ACTION",
+ });
+ }
+
+ await calendar.deleteItem(event);
+}
+
+/**
+ * Test the properties of an event created from a minor or major exception where
+ * we have not added the original event and optionally, the attempt to send a
+ * reply.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doExceptionOnlyTest(conf) {
+ let { calendar, transport, identity, partStat, noReply, isMajor } = conf;
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1)).item;
+
+ // Exceptions are still created as recurring events.
+ Assert.ok(event != event.parentItem, "event created is a recurring event");
+ let occurrences = event.parentItem.recurrenceInfo.getOccurrences(
+ cal.createDateTime("10000101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(occurrences.length, 1, "parent item only has one occurrence");
+ Assert.ok(occurrences[0] == event, "occurrence is the event exception");
+
+ info("Verifying relevant properties of the event");
+ compareProperties(event, {
+ id: "02e79b96",
+ title: isMajor ? event.title : "Exception title",
+ "calendar.name": calendar.name,
+ "recurrenceId.icalString": "20220317T110000Z",
+ "startDate.icalString": isMajor ? "20220317T050000Z" : "20220317T110000Z",
+ "endDate.icalString": isMajor ? "20220317T053000Z" : "20220317T113000Z",
+ description: isMajor ? event.getProperty("DESCRIPTION") : "Exception description",
+ location: isMajor ? event.getProperty("LOCATION") : "Exception location",
+ sequence: isMajor ? "2" : "0",
+ "x-moz-received-dtstamp": isMajor
+ ? event.getProperty("x-moz-received-dtstamp")
+ : "20220318T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ // Alarms should be ignored.
+ Assert.equal(event.getAlarms().length, 0, "event has no reminders");
+
+ info("Verifying attendee list and participation status");
+ let attendees = event.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.participationStatus": partStat,
+ "1.id": "mailto:receiver@example.com",
+ "2.id": "mailto:other@example.com",
+ "2.participationStatus": "NEEDS-ACTION",
+ });
+
+ if (noReply) {
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ } else {
+ await doReplyTest(transport, identity, partStat);
+ }
+ await calendar.deleteItem(event.parentItem);
+}
+
+/**
+ * Tests the recognition and application of a cancellation to an existing event.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doCancelTest({ transport, calendar, isRecurring, event, recurrenceId }) {
+ transport.reset();
+
+ let eventId = event.id;
+ if (isRecurring) {
+ // wait for the other occurrences to appear.
+ await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1);
+ await CalendarTestUtils.monthView.waitForItemAt(window, 3, 6, 1);
+ }
+
+ let cancellationPath = isRecurring
+ ? "data/cancel-repeat-event.eml"
+ : "data/cancel-single-event.eml";
+
+ let cancelMsgFile = new FileUtils.File(getTestFilePath(cancellationPath));
+ if (recurrenceId) {
+ let srcTxt = await IOUtils.readUTF8(cancelMsgFile.path);
+ srcTxt = srcTxt.replaceAll(/RRULE:.+/g, `RECURRENCE-ID:${recurrenceId}`);
+ srcTxt = srcTxt.replaceAll(/SEQUENCE:.+/g, "SEQUENCE:3");
+ cancelMsgFile = FileTestUtils.getTempFile("cancel-occurrence.eml");
+ await IOUtils.writeUTF8(cancelMsgFile.path, srcTxt);
+ }
+
+ let win = await openImipMessage(cancelMsgFile);
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let deleteButton = aboutMessage.document.getElementById("imipDeleteButton");
+ Assert.ok(!deleteButton.hidden, `#${deleteButton.id} button shown`);
+ EventUtils.synthesizeMouseAtCenter(deleteButton, {}, aboutMessage);
+
+ if (isRecurring && recurrenceId) {
+ // Expects a single occurrence to be cancelled.
+
+ let occurrences;
+ await TestUtils.waitForCondition(async () => {
+ let { parentItem } = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ occurrences = parentItem.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ return occurrences.length == 2;
+ }, "occurrence was deleted");
+
+ Assert.ok(
+ occurrences.every(occ => occ.recurrenceId && occ.recurrenceId != recurrenceId),
+ `occurrence "${recurrenceId}" removed`
+ );
+ Assert.ok(!!(await calendar.getItem(eventId)), "event was not deleted");
+ } else {
+ await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 4, 1);
+
+ if (isRecurring) {
+ await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 5, 1);
+ await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 6, 1);
+ }
+
+ await TestUtils.waitForCondition(async () => {
+ let result = await calendar.getItem(eventId);
+ return !result;
+ }, "event was deleted");
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+ Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response");
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+}
+
+/**
+ * Tests processing of cancellations to exceptions to recurring events.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doCancelExceptionTest(conf) {
+ let { partStat, recurrenceId, calendar } = conf;
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, actionIds.recurring.button[partStat]);
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ await BrowserTestUtils.closeWindow(win);
+
+ let update = new FileUtils.File(getTestFilePath("data/exception-major.eml"));
+ let updateWin = await openImipMessage(update);
+ await clickAction(updateWin, actionIds.single.button[partStat]);
+
+ let exception;
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ exception = event.recurrenceInfo.getExceptionFor(cal.createDateTime(recurrenceId));
+ return !!exception;
+ }, "exception applied");
+
+ await BrowserTestUtils.closeWindow(updateWin);
+ await doCancelTest({ ...conf, event });
+ await calendar.deleteItem(event);
+}