summaryrefslogtreecommitdiffstats
path: root/comm/calendar/test/unit/test_invitationutils.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/test/unit/test_invitationutils.js
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/calendar/test/unit/test_invitationutils.js')
-rw-r--r--comm/calendar/test/unit/test_invitationutils.js1654
1 files changed, 1654 insertions, 0 deletions
diff --git a/comm/calendar/test/unit/test_invitationutils.js b/comm/calendar/test/unit/test_invitationutils.js
new file mode 100644
index 0000000000..223ff1a6e6
--- /dev/null
+++ b/comm/calendar/test/unit/test_invitationutils.js
@@ -0,0 +1,1654 @@
+/* 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/. */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+});
+
+function run_test() {
+ do_calendar_startup(run_next_test);
+}
+
+// tests for calInvitationUtils.jsm
+
+// Make sure that the Europe/Berlin timezone and long datetime format is set.
+Services.prefs.setIntPref("calendar.date.format", 0);
+Services.prefs.setStringPref("calendar.timezone.local", "Europe/Berlin");
+
+/**
+ * typedef {Object} FullIcsValue
+ *
+ * @property {Object<string, string>} params - Parameters for the ics property,
+ * mapping from the parameter name to its value. Each name should be in camel
+ * case. For example, to set "PARTSTAT=ACCEPTED" on the "attendee" property,
+ * use `{ partstat: "ACCEPTED" }`.
+ * @property {string} value - The property value.
+ */
+
+/**
+ * An accepted property value.
+ * typedef {(FullIcsValue|string)} IcsValue
+ */
+
+/**
+ * Get a ics string for an event.
+ *
+ * @param {Object<string, (IcsValue | IcsValue[])>} [eventProperties] - Object
+ * used to set the event properties, mapping from the ics property name to its
+ * value. The property name should be in camel case, so "propertyName" should
+ * be used for the "PROPERTY-NAME" property. The value can either be a single
+ * IcsValue, or a IcsValue array if you want more than one such property
+ * in the event (e.g. to set several "attendee" properties). If you give an
+ * empty value for the property, then the property will be excluded.
+ * For the "attendee" and "organizer" properties, "mailto:" will be prefixed
+ * to the value (unless it is empty).
+ * For the "dtstart" and "dtend" properties, the "TZID=Europe/Berlin"
+ * parameter will be set by default.
+ * Some properties will have default values set if they are not specified in
+ * the object. Note that to avoid a property with a default value, you must
+ * pass an empty value for the property.
+ *
+ * @returns {string} - The ics string.
+ */
+function getIcs(eventProperties) {
+ // we use an unfolded ics blueprint here to make replacing of properties easier
+ let item = ["BEGIN:VCALENDAR", "PRODID:-//Google Inc//Google Calendar V1.0//EN", "VERSION:2.0"];
+
+ let eventPropertyNames = eventProperties ? Object.keys(eventProperties) : [];
+
+ // Convert camel case object property name to upper case with dashes.
+ let convertPropertyName = n => n.replace(/[A-Z]/, match => `-${match}`).toUpperCase();
+
+ let propertyToString = (name, value) => {
+ let propertyString = convertPropertyName(name);
+ let setTzid = false;
+ if (typeof value == "object") {
+ for (let paramName in value.params) {
+ if (paramName == "tzid") {
+ setTzid = true;
+ }
+ propertyString += `;${convertPropertyName(paramName)}=${value.params[paramName]}`;
+ }
+ value = value.value;
+ }
+ if (!setTzid && (name == "dtstart" || name == "dtend")) {
+ propertyString += ";TZID=Europe/Berlin";
+ }
+ if (name == "organizer" || name == "attendee") {
+ value = `mailto:${value}`;
+ }
+ return `${propertyString}:${value}`;
+ };
+
+ let appendProperty = (name, value) => {
+ if (!value) {
+ // leave out.
+ return;
+ }
+ if (Array.isArray(value)) {
+ value.forEach(val => item.push(propertyToString(name, val)));
+ } else {
+ item.push(propertyToString(name, value));
+ }
+ };
+
+ let appendPropertyWithDefault = (name, defaultValue) => {
+ let value = defaultValue;
+ let index = eventPropertyNames.findIndex(n => n == name);
+ if (index >= 0) {
+ value = eventProperties[name];
+ // Remove the name to show that we have already handled it.
+ eventPropertyNames.splice(index, 1);
+ }
+ appendProperty(name, value);
+ };
+
+ appendPropertyWithDefault("method", "METHOD:REQUEST");
+
+ item = item.concat([
+ "BEGIN:VTIMEZONE",
+ "TZID:Europe/Berlin",
+ "BEGIN:DAYLIGHT",
+ "TZOFFSETFROM:+0100",
+ "TZOFFSETTO:+0200",
+ "TZNAME:CEST",
+ "DTSTART:19700329T020000",
+ "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU",
+ "END:DAYLIGHT",
+ "BEGIN:STANDARD",
+ "TZOFFSETFROM:+0200",
+ "TZOFFSETTO:+0100",
+ "TZNAME:CET",
+ "DTSTART:19701025T030000",
+ "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU",
+ "END:STANDARD",
+ "END:VTIMEZONE",
+ "BEGIN:VEVENT",
+ ]);
+
+ for (let [name, defaultValue] of [
+ ["created", "20150909T180909Z"],
+ ["lastModified", "20150909T181048Z"],
+ ["dtstamp", "20150909T181048Z"],
+ ["uid", "cb189fdc-ed47-4db6-a8d7-31a08802249d"],
+ ["summary", "Test Event"],
+ [
+ "organizer",
+ {
+ params: { rsvp: "TRUE", cn: "Organizer", partstat: "ACCEPTED", role: "CHAIR" },
+ value: "organizer@example.net",
+ },
+ ],
+ [
+ "attendee",
+ {
+ params: { rsvp: "TRUE", cn: "Attendee", partstat: "NEEDS-ACTION", role: "REQ-PARTICIPANT" },
+ value: "attendee@example.net",
+ },
+ ],
+ ["dtstart", "20150909T210000"],
+ ["dtend", "20150909T220000"],
+ ["sequence", "1"],
+ ["transp", "OPAQUE"],
+ ["location", "Room 1"],
+ ["description", "Let us get together"],
+ ["url", "http://www.example.com"],
+ ["attach", "http://www.example.com"],
+ ]) {
+ appendPropertyWithDefault(name, defaultValue);
+ }
+
+ // Add other properties with no default.
+ for (let name of eventPropertyNames) {
+ appendProperty(name, eventProperties[name]);
+ }
+
+ item.push("END:VEVENT");
+ item.push("END:VCALENDAR");
+
+ return item.join("\r\n");
+}
+
+function getEvent(eventProperties) {
+ let item = getIcs(eventProperties);
+ let itipItem = Cc["@mozilla.org/calendar/itip-item;1"].createInstance(Ci.calIItipItem);
+ itipItem.init(item);
+ let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser);
+ parser.parseString(item);
+ return { event: parser.getItems()[0], itipItem };
+}
+
+add_task(async function getItipHeader_test() {
+ let data = [
+ {
+ name: "Organizer sends invite",
+ input: {
+ method: "REQUEST",
+ attendee: "",
+ },
+ expected: "Organizer has invited you to Test Event",
+ },
+ {
+ name: "Organizer cancels event",
+ input: {
+ method: "CANCEL",
+ attendee: "",
+ },
+ expected: "Organizer has canceled this event: Test Event",
+ },
+ {
+ name: "Organizer declines counter proposal",
+ input: {
+ method: "DECLINECOUNTER",
+ attendee: {
+ params: { rsvp: "TRUE", cn: "Attendee1", partstat: "ACCEPTED", role: "REQ-PARTICIPANT" },
+ value: "attendee1@example.net",
+ },
+ },
+ expected: 'Organizer has declined your counterproposal for "Test Event".',
+ },
+ {
+ name: "Attendee makes counter proposal",
+ input: {
+ method: "COUNTER",
+ attendee: {
+ params: { rsvp: "TRUE", cn: "Attendee1", partstat: "DECLINED", role: "REQ-PARTICIPANT" },
+ value: "attendee1@example.net",
+ },
+ },
+ expected: 'Attendee1 <attendee1@example.net> has made a counterproposal for "Test Event":',
+ },
+ {
+ name: "Attendee replies with acceptance",
+ input: {
+ method: "REPLY",
+ attendee: {
+ params: { rsvp: "TRUE", cn: "Attendee1", partstat: "ACCEPTED", role: "REQ-PARTICIPANT" },
+ value: "attendee1@example.net",
+ },
+ },
+ expected: "Attendee1 <attendee1@example.net> has accepted your event invitation.",
+ },
+ {
+ name: "Attendee replies with tentative acceptance",
+ input: {
+ method: "REPLY",
+ attendee: {
+ params: { rsvp: "TRUE", cn: "Attendee1", partstat: "TENTATIVE", role: "REQ-PARTICIPANT" },
+ value: "attendee1@example.net",
+ },
+ },
+ expected: "Attendee1 <attendee1@example.net> has accepted your event invitation.",
+ },
+ {
+ name: "Attendee replies with declined",
+ input: {
+ method: "REPLY",
+ attendee: {
+ params: { rsvp: "TRUE", cn: "Attendee1", partstat: "DECLINED", role: "REQ-PARTICIPANT" },
+ value: "attendee1@example.net",
+ },
+ },
+ expected: "Attendee1 <attendee1@example.net> has declined your event invitation.",
+ },
+ {
+ name: "Attendee1 accepts and Attendee2 declines",
+ input: {
+ method: "REPLY",
+ attendee: [
+ {
+ params: {
+ rsvp: "TRUE",
+ cn: "Attendee1",
+ partstat: "ACCEPTED",
+ role: "REQ-PARTICIPANT",
+ },
+ value: "attendee1@example.net",
+ },
+ {
+ params: {
+ rsvp: "TRUE",
+ cn: "Attendee2",
+ partstat: "DECLINED",
+ role: "REQ-PARTICIPANT",
+ },
+ value: "attendee2@example.net",
+ },
+ ],
+ },
+ expected: "Attendee1 <attendee1@example.net> has accepted your event invitation.",
+ },
+ {
+ name: "Unsupported method",
+ input: {
+ method: "UNSUPPORTED",
+ attendee: "",
+ },
+ expected: "Event Invitation",
+ },
+ {
+ name: "No method",
+ input: {
+ method: "",
+ attendee: "",
+ },
+ expected: "Event Invitation",
+ },
+ ];
+ for (let test of data) {
+ let itipItem = Cc["@mozilla.org/calendar/itip-item;1"].createInstance(Ci.calIItipItem);
+ let item = getIcs(test.input);
+ itipItem.init(item);
+ if (test.input.attendee) {
+ let sender = new CalAttendee();
+ sender.icalString = item.match(/^ATTENDEE.*$/m)[0];
+ itipItem.sender = sender.id;
+ }
+ equal(cal.invitation.getItipHeader(itipItem), test.expected, `(test ${test.name})`);
+ }
+});
+
+function assertHiddenRow(node, hidden, testName) {
+ let row = node.closest("tr");
+ ok(row, `Row above ${node.id} should exist (test ${testName})`);
+ if (hidden) {
+ equal(
+ node.textContent,
+ "",
+ `Node ${node.id} should be empty below a hidden row (test ${testName})`
+ );
+ ok(row.hidden, `Row above ${node.id} should be hidden (test ${testName})`);
+ } else {
+ ok(!row.hidden, `Row above ${node.id} should not be hidden (test ${testName})`);
+ }
+}
+
+add_task(async function createInvitationOverlay_test() {
+ let data = [
+ {
+ name: "No description",
+ input: { description: "" },
+ expected: { node: "imipHtml-description-content", hidden: true },
+ },
+ {
+ name: "Description with https link",
+ input: { description: "Go to https://www.example.net if you can." },
+ expected: {
+ node: "imipHtml-description-content",
+ content:
+ 'Go to <a class="moz-txt-link-freetext" href="https://www.example.net">' +
+ "https://www.example.net</a> if you can.",
+ },
+ },
+ {
+ name: "Description plain link",
+ input: { description: "Go to www.example.net if you can." },
+ expected: {
+ node: "imipHtml-description-content",
+ content:
+ 'Go to <a class="moz-txt-link-abbreviated" href="http://www.example.net">' +
+ "www.example.net</a> if you can.",
+ },
+ },
+ {
+ name: "Description with +/-",
+ input: { description: "Let's see if +/- still can be displayed." },
+ expected: {
+ node: "imipHtml-description-content",
+ content: "Let's see if +/- still can be displayed.",
+ },
+ },
+ {
+ name: "Description with mailto",
+ input: { description: "Or write to mailto:faq@example.net instead." },
+ expected: {
+ node: "imipHtml-description-content",
+ content:
+ 'Or write to <a class="moz-txt-link-freetext" ' +
+ 'href="mailto:faq@example.net">mailto:faq@example.net</a> instead.',
+ },
+ },
+ {
+ name: "Description with email",
+ input: { description: "Or write to faq@example.net instead." },
+ expected: {
+ node: "imipHtml-description-content",
+ content:
+ 'Or write to <a class="moz-txt-link-abbreviated" ' +
+ 'href="mailto:faq@example.net">faq@example.net</a> instead.',
+ },
+ },
+ {
+ name: "Description with emoticon",
+ input: { description: "It's up to you ;-)" },
+ expected: {
+ node: "imipHtml-description-content",
+ content: "It's up to you ;-)",
+ },
+ },
+ {
+ name: "Removed script injection from description",
+ input: {
+ description:
+ 'Let\'s see how evil we can be: <script language="JavaScript">' +
+ 'document.getElementById("imipHtml-description-content")' +
+ '.write("Script embedded!")</script>',
+ },
+ expected: {
+ node: "imipHtml-description-content",
+ content: "Let's see how evil we can be: ",
+ },
+ },
+ {
+ name: "Removed img src injection from description",
+ input: {
+ description:
+ 'Or we can try: <img src="document.getElementById("imipHtml-' +
+ 'description-descr").innerText" >',
+ },
+ expected: {
+ node: "imipHtml-description-content",
+ content: "Or we can try: ",
+ },
+ },
+ {
+ name: "Description with special characters",
+ input: {
+ description:
+ 'Check <a href="http://example.com">example.com</a>&nbsp;&nbsp;&mdash; only 3 &euro;',
+ },
+ expected: {
+ node: "imipHtml-description-content",
+ content: 'Check <a href="http://example.com">example.com</a>&nbsp;&nbsp;— only 3 €',
+ },
+ },
+ {
+ name: "URL",
+ input: { url: "http://www.example.org/event.ics" },
+ expected: {
+ node: "imipHtml-url-content",
+ content:
+ '<a class="moz-txt-link-freetext" href="http://www.example.org/event.ics">' +
+ "http://www.example.org/event.ics</a>",
+ },
+ },
+ {
+ name: "URL attachment",
+ input: { attach: "http://www.example.org" },
+ expected: {
+ node: "imipHtml-attachments-content",
+ content:
+ '<a class="moz-txt-link-freetext" href="http://www.example.org/">' +
+ "http://www.example.org/</a>",
+ },
+ },
+ {
+ name: "Non-URL attachment is ignored",
+ input: {
+ attach: {
+ params: { fmttype: "text/plain", encoding: "BASE64", value: "BINARY" },
+ value: "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4",
+ },
+ },
+ expected: { node: "imipHtml-attachments-content", hidden: true },
+ },
+ {
+ name: "Several attachments",
+ input: {
+ attach: [
+ "http://www.example.org/first/",
+ "http://www.example.org/second",
+ "file:///N:/folder/third.file",
+ ],
+ },
+ expected: {
+ node: "imipHtml-attachments-content",
+ content:
+ '<a class="moz-txt-link-freetext" href="http://www.example.org/first/">' +
+ "http://www.example.org/first/</a><br>" +
+ '<a class="moz-txt-link-freetext" href="http://www.example.org/second">' +
+ "http://www.example.org/second</a><br>" +
+ '<a class="moz-txt-link-freetext">file:///N:/folder/third.file</a>',
+ },
+ },
+ {
+ name: "Attendees",
+ input: {
+ attendee: [
+ {
+ params: {
+ rsvp: "TRUE",
+ partstat: "NEEDS-ACTION",
+ role: "OPT-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ cn: '"Attendee 1"',
+ },
+ value: "attendee1@example.net",
+ },
+ {
+ params: {
+ rsvp: "TRUE",
+ partstat: "ACCEPTED",
+ role: "NON-PARTICIPANT",
+ cutype: "GROUP",
+ },
+ value: "attendee2@example.net",
+ },
+ {
+ params: {
+ rsvp: "TRUE",
+ partstat: "TENTATIVE",
+ role: "REQ-PARTICIPANT",
+ cutype: "RESOURCE",
+ },
+ value: "attendee3@example.net",
+ },
+ {
+ params: {
+ rsvp: "TRUE",
+ partstat: "DECLINED",
+ role: "OPT-PARTICIPANT",
+ delegatedFrom: '"mailto:attendee5@example.net"',
+ cutype: "ROOM",
+ },
+ value: "attendee4@example.net",
+ },
+ {
+ params: {
+ rsvp: "TRUE",
+ partstat: "DELEGATED",
+ role: "OPT-PARTICIPANT",
+ delegatedTo: '"mailto:attendee4@example.net"',
+ cutype: "UNKNOWN",
+ },
+ value: "attendee5@example.net",
+ },
+ {
+ params: { rsvp: "TRUE" },
+ value: "attendee6@example.net",
+ },
+ "attendee7@example.net",
+ ],
+ },
+ expected: {
+ node: "imipHtml-attendees-cell",
+ attendeesList: [
+ {
+ name: "Attendee 1 <attendee1@example.net>",
+ title:
+ "Attendee 1 <attendee1@example.net> is an optional " +
+ "participant. Attendee 1 still needs to reply.",
+ icon: {
+ attendeerole: "OPT-PARTICIPANT",
+ usertype: "INDIVIDUAL",
+ partstat: "NEEDS-ACTION",
+ },
+ },
+ {
+ name: "attendee2@example.net",
+ title:
+ "attendee2@example.net (group) is a non-participant. " +
+ "attendee2@example.net has confirmed attendance.",
+ icon: {
+ attendeerole: "NON-PARTICIPANT",
+ usertype: "GROUP",
+ partstat: "ACCEPTED",
+ },
+ },
+ {
+ name: "attendee3@example.net",
+ title:
+ "attendee3@example.net (resource) is a required " +
+ "participant. attendee3@example.net has confirmed attendance " +
+ "tentatively.",
+ icon: {
+ attendeerole: "REQ-PARTICIPANT",
+ usertype: "RESOURCE",
+ partstat: "TENTATIVE",
+ },
+ },
+ {
+ name: "attendee4@example.net (delegated from attendee5@example.net)",
+ title:
+ "attendee4@example.net (room) is an optional participant. " +
+ "attendee4@example.net has declined attendance.",
+ icon: {
+ attendeerole: "OPT-PARTICIPANT",
+ usertype: "ROOM",
+ partstat: "DECLINED",
+ },
+ },
+ {
+ name: "attendee5@example.net",
+ title:
+ "attendee5@example.net is an optional participant. " +
+ "attendee5@example.net has delegated attendance to " +
+ "attendee4@example.net.",
+ icon: {
+ attendeerole: "OPT-PARTICIPANT",
+ usertype: "UNKNOWN",
+ partstat: "DELEGATED",
+ },
+ },
+ {
+ name: "attendee6@example.net",
+ title:
+ "attendee6@example.net is a required participant. " +
+ "attendee6@example.net still needs to reply.",
+ icon: {
+ attendeerole: "REQ-PARTICIPANT",
+ usertype: "INDIVIDUAL",
+ partstat: "NEEDS-ACTION",
+ },
+ },
+ {
+ name: "attendee7@example.net",
+ title:
+ "attendee7@example.net is a required participant. " +
+ "attendee7@example.net still needs to reply.",
+ icon: {
+ attendeerole: "REQ-PARTICIPANT",
+ usertype: "INDIVIDUAL",
+ partstat: "NEEDS-ACTION",
+ },
+ },
+ ],
+ },
+ },
+ {
+ name: "Organizer",
+ input: {
+ organizer: {
+ params: {
+ partstat: "ACCEPTED",
+ role: "CHAIR",
+ cutype: "INDIVIDUAL",
+ cn: '"The Organizer"',
+ },
+ value: "organizer@example.net",
+ },
+ },
+ expected: {
+ node: "imipHtml-organizer-cell",
+ organizer: {
+ name: "The Organizer <organizer@example.net>",
+ title:
+ "The Organizer <organizer@example.net> chairs the event. " +
+ "The Organizer has confirmed attendance.",
+ icon: {
+ attendeerole: "CHAIR",
+ usertype: "INDIVIDUAL",
+ partstat: "ACCEPTED",
+ },
+ },
+ },
+ },
+ ];
+
+ function assertAttendee(attendee, name, title, icon, testName) {
+ equal(attendee.textContent, name, `Attendee names (test ${testName})`);
+ equal(attendee.getAttribute("title"), title, `Title for ${name} (test ${testName})`);
+ let attendeeIcon = attendee.querySelector(".itip-icon");
+ ok(attendeeIcon, `icon for ${name} should exist (test ${testName})`);
+ for (let attr in icon) {
+ equal(
+ attendeeIcon.getAttribute(attr),
+ icon[attr],
+ `${attr} for icon for ${name} (test ${testName})`
+ );
+ }
+ }
+
+ for (let test of data) {
+ info(`testing ${test.name}`);
+ let { event, itipItem } = getEvent(test.input);
+ let dom = cal.invitation.createInvitationOverlay(event, itipItem);
+ let node = dom.getElementById(test.expected.node);
+ ok(node, `Element with id ${test.expected.node} should exist (test ${test.name})`);
+ if (test.expected.hidden) {
+ assertHiddenRow(node, true, test.name);
+ continue;
+ }
+ assertHiddenRow(node, false, test.name);
+
+ if ("attendeesList" in test.expected) {
+ let attendeeNodes = node.querySelectorAll(".attendee-label");
+ // Assert same order.
+ let i;
+ for (i = 0; i < test.expected.attendeesList.length; i++) {
+ let { name, title, icon } = test.expected.attendeesList[i];
+ ok(
+ attendeeNodes.length > i,
+ `Enough attendees for expected attendee #${i} ${name} (test ${test.name})`
+ );
+ assertAttendee(attendeeNodes[i], name, title, icon, test.name);
+ }
+ equal(attendeeNodes.length, i, `Same number of attendees (test ${test.name})`);
+ } else if ("organizer" in test.expected) {
+ let { name, title, icon } = test.expected.organizer;
+ let organizerNode = node.querySelector(".attendee-label");
+ ok(organizerNode, `Organizer node should exist (test ${test.name})`);
+ assertAttendee(organizerNode, name, title, icon, test.name);
+ } else {
+ equal(node.innerHTML, test.expected.content, `innerHTML (test ${test.name})`);
+ }
+ }
+});
+
+add_task(async function updateInvitationOverlay_test() {
+ let data = [
+ {
+ name: "No description before or after",
+ input: { previous: { description: "" }, current: { description: "" } },
+ expected: { node: "imipHtml-description-content", hidden: true },
+ },
+ {
+ name: "Same description before and after",
+ input: {
+ previous: { description: "This is the description" },
+ current: { description: "This is the description" },
+ },
+ expected: {
+ node: "imipHtml-description-content",
+ content: [{ type: "same", text: "This is the description" }],
+ },
+ },
+ {
+ name: "Added description",
+ input: {
+ previous: { description: "" },
+ current: { description: "Added this description" },
+ },
+ expected: {
+ node: "imipHtml-description-content",
+ content: [{ type: "added", text: "Added this description" }],
+ },
+ },
+ {
+ name: "Removed description",
+ input: {
+ previous: { description: "Removed this description" },
+ current: { description: "" },
+ },
+ expected: {
+ node: "imipHtml-description-content",
+ content: [{ type: "removed", text: "Removed this description" }],
+ },
+ },
+ {
+ name: "Location",
+ input: {
+ previous: { location: "This place" },
+ current: { location: "Another location" },
+ },
+ expected: {
+ node: "imipHtml-location-content",
+ content: [
+ { type: "added", text: "Another location" },
+ { type: "removed", text: "This place" },
+ ],
+ },
+ },
+ {
+ name: "Summary",
+ input: {
+ previous: { summary: "My invitation" },
+ current: { summary: "My new invitation" },
+ },
+ expected: {
+ node: "imipHtml-summary-content",
+ content: [
+ { type: "added", text: "My new invitation" },
+ { type: "removed", text: "My invitation" },
+ ],
+ },
+ },
+ {
+ name: "When",
+ input: {
+ previous: {
+ dtstart: "20150909T130000",
+ dtend: "20150909T140000",
+ },
+ current: {
+ dtstart: "20150909T140000",
+ dtend: "20150909T150000",
+ },
+ },
+ expected: {
+ node: "imipHtml-when-content",
+ content: [
+ // Time format is platform dependent, so we use alternative result
+ // sets here.
+ // If you get a failure for this test, add your pattern here.
+ {
+ type: "added",
+ text: /^Wednesday, (September 0?9,|0?9 September) 2015 (2:00 PM – 3:00 PM|14:00 – 15:00)$/,
+ },
+ {
+ type: "removed",
+ text: /^Wednesday, (September 0?9,|0?9 September) 2015 (1:00 PM – 2:00 PM|13:00 – 14:00)$/,
+ },
+ ],
+ },
+ },
+ {
+ name: "Organizer same",
+ input: {
+ previous: { organizer: "organizer1@example.net" },
+ current: { organizer: "organizer1@example.net" },
+ },
+ expected: {
+ node: "imipHtml-organizer-cell",
+ organizer: [{ type: "same", text: "organizer1@example.net" }],
+ },
+ },
+ {
+ name: "Organizer modified",
+ input: {
+ // Modify ROLE from CHAIR to REQ-PARTICIPANT.
+ previous: { organizer: { params: { role: "CHAIR" }, value: "organizer1@example.net" } },
+ current: {
+ organizer: { params: { role: "REQ-PARTICIPANT" }, value: "organizer1@example.net" },
+ },
+ },
+ expected: {
+ node: "imipHtml-organizer-cell",
+ organizer: [{ type: "modified", text: "organizer1@example.net" }],
+ },
+ },
+ {
+ name: "Organizer added",
+ input: {
+ previous: { organizer: "" },
+ current: { organizer: "organizer2@example.net" },
+ },
+ expected: {
+ node: "imipHtml-organizer-cell",
+ organizer: [{ type: "added", text: "organizer2@example.net" }],
+ },
+ },
+ {
+ name: "Organizer removed",
+ input: {
+ previous: { organizer: "organizer2@example.net" },
+ current: { organizer: "" },
+ },
+ expected: {
+ node: "imipHtml-organizer-cell",
+ organizer: [{ type: "removed", text: "organizer2@example.net" }],
+ },
+ },
+ {
+ name: "Organizer changed",
+ input: {
+ previous: { organizer: "organizer1@example.net" },
+ current: { organizer: "organizer2@example.net" },
+ },
+ expected: {
+ node: "imipHtml-organizer-cell",
+ organizer: [
+ { type: "added", text: "organizer2@example.net" },
+ { type: "removed", text: "organizer1@example.net" },
+ ],
+ },
+ },
+ {
+ name: "Attendees: modify one, remove one, add one",
+ input: {
+ previous: {
+ attendee: [
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee1@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee2@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee3@example.net",
+ },
+ ],
+ },
+ current: {
+ attendee: [
+ {
+ // Modify PARTSTAT from NEEDS-ACTION.
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "ACCEPTED" },
+ value: "attendee2@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee3@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee4@example.net",
+ },
+ ],
+ },
+ },
+ expected: {
+ node: "imipHtml-attendees-cell",
+ attendeesList: [
+ { type: "removed", text: "attendee1@example.net" },
+ { type: "modified", text: "attendee2@example.net" },
+ { type: "same", text: "attendee3@example.net" },
+ { type: "added", text: "attendee4@example.net" },
+ ],
+ },
+ },
+ {
+ name: "Attendees: modify one, remove three, add two",
+ input: {
+ previous: {
+ attendee: [
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee-remove1@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "GROUP", partstat: "NEEDS-ACTION" },
+ value: "attendee1@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee-remove2@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee-remove3@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee3@example.net",
+ },
+ ],
+ },
+ current: {
+ attendee: [
+ {
+ // Modify CUTYPE from GROUP.
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee1@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee-add1@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee-add2@example.net",
+ },
+ {
+ params: { rsvp: "TRUE", cutype: "INDIVIDUAL", partstat: "NEEDS-ACTION" },
+ value: "attendee3@example.net",
+ },
+ ],
+ },
+ },
+ expected: {
+ node: "imipHtml-attendees-cell",
+ attendeesList: [
+ { type: "removed", text: "attendee-remove1@example.net" },
+ { type: "modified", text: "attendee1@example.net" },
+ // Added shown first, then removed, and in between the common
+ // attendees.
+ { type: "added", text: "attendee-add1@example.net" },
+ { type: "added", text: "attendee-add2@example.net" },
+ { type: "removed", text: "attendee-remove2@example.net" },
+ { type: "removed", text: "attendee-remove3@example.net" },
+ { type: "same", text: "attendee3@example.net" },
+ ],
+ },
+ },
+ ];
+
+ function assertElement(node, text, type, testName) {
+ let found = node.textContent;
+ if (text instanceof RegExp) {
+ ok(text.test(found), `Text content "${found}" matches regex (test ${testName})`);
+ } else {
+ equal(text, found, `Text content matches (test ${testName})`);
+ }
+ switch (type) {
+ case "added":
+ equal(node.tagName, "INS", `Text "${text}" is inserted (test ${testName})`);
+ ok(node.classList.contains("added"), `Text "${text}" is added (test ${testName})`);
+ break;
+ case "removed":
+ equal(node.tagName, "DEL", `Text "${text}" is deleted (test ${testName})`);
+ ok(node.classList.contains("removed"), `Text "${text}" is removed (test ${testName})`);
+ break;
+ case "modified":
+ ok(node.tagName !== "DEL", `Text "${text}" is not deleted (test ${testName})`);
+ ok(node.tagName !== "INS", `Text "${text}" is not inserted (test ${testName})`);
+ ok(node.classList.contains("modified"), `Text "${text}" is modified (test ${testName})`);
+ break;
+ case "same":
+ // NOTE: node may be a Text node.
+ ok(node.tagName !== "DEL", `Text "${text}" is not deleted (test ${testName})`);
+ ok(node.tagName !== "INS", `Text "${text}" is not inserted (test ${testName})`);
+ if (node.classList) {
+ ok(!node.classList.contains("added"), `Text "${text}" is not added (test ${testName})`);
+ ok(
+ !node.classList.contains("removed"),
+ `Text "${text}" is not removed (test ${testName})`
+ );
+ ok(
+ !node.classList.contains("modified"),
+ `Text "${text}" is not modified (test ${testName})`
+ );
+ }
+ break;
+ default:
+ ok(false, `Unknown type ${type} for text "${text}" (test ${testName})`);
+ break;
+ }
+ }
+
+ for (let test of data) {
+ info(`testing ${test.name}`);
+ let { event, itipItem } = getEvent(test.input.current);
+ let dom = cal.invitation.createInvitationOverlay(event, itipItem);
+ let { event: oldEvent } = getEvent(test.input.previous);
+ cal.invitation.updateInvitationOverlay(dom, event, itipItem, oldEvent);
+
+ let node = dom.getElementById(test.expected.node);
+ ok(node, `Element with id ${test.expected.node} should exist (test ${test.name})`);
+ if (test.expected.hidden) {
+ assertHiddenRow(node, true, test.name);
+ continue;
+ }
+ assertHiddenRow(node, false, test.name);
+
+ let insertBreaks = false;
+ let nodeList;
+ let expectList;
+
+ if ("attendeesList" in test.expected) {
+ // Insertions, deletions and modifications are all within separate
+ // list-items.
+ nodeList = node.querySelectorAll(":scope > .attendee-list > .attendee-list-item > *");
+ expectList = test.expected.attendeesList;
+ } else if ("organizer" in test.expected) {
+ nodeList = node.childNodes;
+ expectList = test.expected.organizer;
+ } else {
+ nodeList = node.childNodes;
+ expectList = test.expected.content;
+ insertBreaks = true;
+ }
+
+ // Assert in same order.
+ let first = true;
+ let nodeIndex = 0;
+ for (let { text, type } of expectList) {
+ if (first) {
+ first = false;
+ } else if (insertBreaks) {
+ ok(
+ nodeList.length > nodeIndex,
+ `Enough child nodes for expected break node at index ${nodeIndex} (test ${test.name})`
+ );
+ equal(
+ nodeList[nodeIndex].tagName,
+ "BR",
+ `Break node at index ${nodeIndex} (test ${test.name})`
+ );
+ nodeIndex++;
+ }
+
+ ok(
+ nodeList.length > nodeIndex,
+ `Enough child nodes for expected node at index ${nodeIndex} "${text}" (test ${test.name})`
+ );
+ assertElement(nodeList[nodeIndex], text, type, test.name);
+ nodeIndex++;
+ }
+ equal(nodeList.length, nodeIndex, `Covered all nodes (test ${test.name})`);
+ }
+});
+
+add_task(async function getHeaderSection_test() {
+ let data = [
+ {
+ // test #1
+ input: {
+ toList: "recipient@example.net",
+ subject: "Invitation: test subject",
+ identity: {
+ fullName: "Invitation sender",
+ email: "sender@example.net",
+ replyTo: "no-reply@example.net",
+ organization: "Example Net",
+ cc: "cc@example.net",
+ bcc: "bcc@example.net",
+ },
+ },
+ expected:
+ "MIME-version: 1.0\r\n" +
+ "Return-path: no-reply@example.net\r\n" +
+ "From: Invitation sender <sender@example.net>\r\n" +
+ "Organization: Example Net\r\n" +
+ "To: recipient@example.net\r\n" +
+ "Subject: Invitation: test subject\r\n" +
+ "Cc: cc@example.net\r\n" +
+ "Bcc: bcc@example.net\r\n",
+ },
+ {
+ // test #2
+ input: {
+ toList: 'rec1@example.net, Recipient 2 <rec2@example.net>, "Rec, 3" <rec3@example.net>',
+ subject: "Invitation: test subject",
+ identity: {
+ fullName: '"invitation, sender"',
+ email: "sender@example.net",
+ replyTo: "no-reply@example.net",
+ organization: "Example Net",
+ cc: 'cc1@example.net, Cc 2 <cc2@example.net>, "Cc, 3" <cc3@example.net>',
+ bcc: 'bcc1@example.net, BCc 2 <bcc2@example.net>, "Bcc, 3" <bcc3@example.net>',
+ },
+ },
+ expected:
+ "MIME-version: 1.0\r\n" +
+ "Return-path: no-reply@example.net\r\n" +
+ 'From: "invitation, sender" <sender@example.net>\r\n' +
+ "Organization: Example Net\r\n" +
+ 'To: rec1@example.net, Recipient 2 <rec2@example.net>,\r\n "Rec, 3" <rec3@example.net>\r\n' +
+ "Subject: Invitation: test subject\r\n" +
+ 'Cc: cc1@example.net, Cc 2 <cc2@example.net>, "Cc, 3" <cc3@example.net>\r\n' +
+ 'Bcc: bcc1@example.net, BCc 2 <bcc2@example.net>, "Bcc, 3"\r\n <bcc3@example.net>\r\n',
+ },
+ {
+ // test #3
+ input: {
+ toList: "recipient@example.net",
+ subject: "Invitation: test subject",
+ identity: { email: "sender@example.net" },
+ },
+ expected:
+ "MIME-version: 1.0\r\n" +
+ "From: sender@example.net\r\n" +
+ "To: recipient@example.net\r\n" +
+ "Subject: Invitation: test subject\r\n",
+ },
+ {
+ // test #4
+ input: {
+ toList: "Max Müller <mueller@example.net>",
+ subject: "Invitation: Diacritis check (üäé)",
+ identity: {
+ fullName: "René",
+ email: "sender@example.net",
+ replyTo: "Max & René <no-reply@example.net>",
+ organization: "Max & René",
+ cc: "René <cc@example.net>",
+ bcc: "René <bcc@example.net>",
+ },
+ },
+ expected:
+ "MIME-version: 1.0\r\n" +
+ "Return-path: =?UTF-8?B?TWF4ICYgUmVuw6k=?= <no-reply@example.net>\r\n" +
+ "From: =?UTF-8?B?UmVuw6k=?= <sender@example.net>\r\n" +
+ "Organization: =?UTF-8?B?TWF4ICYgUmVuw6k=?=\r\n" +
+ "To: =?UTF-8?Q?Max_M=C3=BCller?= <mueller@example.net>\r\n" +
+ "Subject: =?UTF-8?B?SW52aXRhdGlvbjogRGlhY3JpdGlzIGNoZWNrICjDvMOk?=\r\n =?UTF-8?B" +
+ "?w6kp?=\r\n" +
+ "Cc: =?UTF-8?B?UmVuw6k=?= <cc@example.net>\r\n" +
+ "Bcc: =?UTF-8?B?UmVuw6k=?= <bcc@example.net>\r\n",
+ },
+ ];
+ let i = 0;
+ for (let test of data) {
+ i++;
+ info(`Running test #${i}`);
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = test.input.identity.email || null;
+ identity.fullName = test.input.identity.fullName || null;
+ identity.replyTo = test.input.identity.replyTo || null;
+ identity.organization = test.input.identity.organization || null;
+ identity.doCc = test.input.identity.doCc || test.input.identity.cc;
+ identity.doCcList = test.input.identity.cc || null;
+ identity.doBcc = test.input.identity.doBcc || test.input.identity.bcc;
+ identity.doBccList = test.input.identity.bcc || null;
+
+ let composeUtils = Cc["@mozilla.org/messengercompose/computils;1"].createInstance(
+ Ci.nsIMsgCompUtils
+ );
+ let messageId = composeUtils.msgGenerateMessageId(identity, null);
+
+ let header = cal.invitation.getHeaderSection(
+ messageId,
+ identity,
+ test.input.toList,
+ test.input.subject
+ );
+ // we test Date and Message-ID headers separately to avoid false positives
+ ok(!!header.match(/Date:.+(?:\n|\r\n|\r)/), "(test #" + i + "): date");
+ ok(!!header.match(/Message-ID:.+(?:\n|\r\n|\r)/), "(test #" + i + "): message-id");
+ equal(
+ header.replace(/Date:.+(?:\n|\r\n|\r)/, "").replace(/Message-ID:.+(?:\n|\r\n|\r)/, ""),
+ test.expected.replace(/Date:.+(?:\n|\r\n|\r)/, "").replace(/Message-ID:.+(?:\n|\r\n|\r)/, ""),
+ "(test #" + i + "): all headers"
+ );
+ }
+});
+
+add_task(async function convertFromUnicode_test() {
+ let data = [
+ {
+ // test #1
+ input: "müller",
+ expected: "müller",
+ },
+ {
+ // test #2
+ input: "muller",
+ expected: "muller",
+ },
+ {
+ // test #3
+ input: "müller\nmüller",
+ expected: "müller\nmüller",
+ },
+ {
+ // test #4
+ input: "müller\r\nmüller",
+ expected: "müller\r\nmüller",
+ },
+ ];
+ let i = 0;
+ for (let test of data) {
+ i++;
+ equal(cal.invitation.convertFromUnicode(test.input), test.expected, "(test #" + i + ")");
+ }
+});
+
+add_task(async function encodeUTF8_test() {
+ let data = [
+ {
+ // test #1
+ input: "müller",
+ expected: "müller",
+ },
+ {
+ // test #2
+ input: "muller",
+ expected: "muller",
+ },
+ {
+ // test #3
+ input: "müller\nmüller",
+ expected: "müller\r\nmüller",
+ },
+ {
+ // test #4
+ input: "müller\r\nmüller",
+ expected: "müller\r\nmüller",
+ },
+ {
+ // test #5
+ input: "",
+ expected: "",
+ },
+ ];
+ let i = 0;
+ for (let test of data) {
+ i++;
+ equal(cal.invitation.encodeUTF8(test.input), test.expected, "(test #" + i + ")");
+ }
+});
+
+add_task(async function encodeMimeHeader_test() {
+ let data = [
+ {
+ // test #1
+ input: {
+ header: "Max Müller <m.mueller@example.net>",
+ isEmail: true,
+ },
+ expected: "=?UTF-8?Q?Max_M=C3=BCller?= <m.mueller@example.net>",
+ },
+ {
+ // test #2
+ input: {
+ header: "Max Mueller <m.mueller@example.net>",
+ isEmail: true,
+ },
+ expected: "Max Mueller <m.mueller@example.net>",
+ },
+ {
+ // test #3
+ input: {
+ header: "Müller & Müller",
+ isEmail: false,
+ },
+ expected: "=?UTF-8?B?TcO8bGxlciAmIE3DvGxsZXI=?=",
+ },
+ ];
+
+ let i = 0;
+ for (let test of data) {
+ i++;
+ equal(
+ cal.invitation.encodeMimeHeader(test.input.header, test.input.isEmail),
+ test.expected,
+ "(test #" + i + ")"
+ );
+ }
+});
+
+add_task(async function getRfc5322FormattedDate_test() {
+ let data = {
+ input: [
+ {
+ // test #1
+ date: null,
+ timezone: "America/New_York",
+ },
+ {
+ // test #2
+ date: "Sat, 24 Jan 2015 09:24:49 +0100",
+ timezone: "America/New_York",
+ },
+ {
+ // test #3
+ date: "Sat, 24 Jan 2015 09:24:49 GMT+0100",
+ timezone: "America/New_York",
+ },
+ {
+ // test #4
+ date: "Sat, 24 Jan 2015 09:24:49 GMT",
+ timezone: "America/New_York",
+ },
+ {
+ // test #5
+ date: "Sat, 24 Jan 2015 09:24:49",
+ timezone: "America/New_York",
+ },
+ {
+ // test #6
+ date: "Sat, 24 Jan 2015 09:24:49",
+ timezone: null,
+ },
+ {
+ // test #7
+ date: "Sat, 24 Jan 2015 09:24:49",
+ timezone: "UTC",
+ },
+ {
+ // test #8
+ date: "Sat, 24 Jan 2015 09:24:49",
+ timezone: "floating",
+ },
+ ],
+ expected: /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}$/,
+ };
+
+ let i = 0;
+ let timezone = Services.prefs.getStringPref("calendar.timezone.local", null);
+ for (let test of data.input) {
+ i++;
+ if (test.timezone) {
+ Services.prefs.setStringPref("calendar.timezone.local", test.timezone);
+ } else {
+ Services.prefs.clearUserPref("calendar.timezone.local");
+ }
+ let date = test.date ? new Date(test.date) : null;
+ let re = new RegExp(data.expected);
+ ok(re.test(cal.invitation.getRfc5322FormattedDate(date)), "(test #" + i + ")");
+ }
+ Services.prefs.setStringPref("calendar.timezone.local", timezone);
+});
+
+add_task(async function parseCounter_test() {
+ // We are disabling this rule for a more consistent display of this data
+ /* eslint-disable object-curly-newline */
+ let data = [
+ {
+ name: "Basic test to check all currently supported properties",
+ input: {
+ proposed: {
+ method: "COUNTER",
+ dtstart: "20150910T210000",
+ dtend: "20150910T220000",
+ location: "Room 2",
+ summary: "Test Event 2",
+ attendee: {
+ params: { cn: "Attendee", partstat: "DECLINED", role: "REQ-PARTICIPANT" },
+ value: "attendee@example.net",
+ },
+ dtstamp: "20150909T182048Z",
+ comment: "Sorry, I cannot make it that time.",
+ },
+ },
+ expected: {
+ // Time format is platform dependent, so we use alternative result sets here.
+ // The first two are configurations running for automated tests.
+ // If you get a failure for this test, add your pattern here.
+ result: { descr: "", type: "OK" },
+ differences: {
+ summary: {
+ proposed: "Test Event 2",
+ original: "Test Event",
+ },
+ location: {
+ proposed: "Room 2",
+ original: "Room 1",
+ },
+ dtstart: {
+ proposed:
+ /^Thursday, (September 10,|10 September) 2015 (9:00 PM|21:00) Europe\/Berlin$/,
+ original:
+ /^Wednesday, (September 0?9,|0?9 September) 2015 (9:00 PM|21:00) Europe\/Berlin$/,
+ },
+ dtend: {
+ proposed:
+ /^Thursday, (September 10,|10 September) 2015 (10:00 PM|22:00) Europe\/Berlin$/,
+ original:
+ /^Wednesday, (September 0?9,|0?9 September) 2015 (10:00 PM|22:00) Europe\/Berlin$/,
+ },
+ comment: {
+ proposed: "Sorry, I cannot make it that time.",
+ original: null,
+ },
+ },
+ },
+ },
+ {
+ name: "Test with an unsupported property has been changed",
+ input: {
+ proposed: {
+ method: "COUNTER",
+ attendee: {
+ params: { cn: "Attendee", partstat: "NEEDS-ACTION", role: "REQ-PARTICIPANT" },
+ value: "attendee@example.net",
+ },
+ location: "Room 2",
+ attach: "http://www.example2.com",
+ dtstamp: "20150909T182048Z",
+ },
+ },
+ expected: {
+ result: { descr: "", type: "OK" },
+ differences: { location: { proposed: "Room 2", original: "Room 1" } },
+ },
+ },
+ {
+ name: "Proposed change not based on the latest update of the invitation",
+ input: {
+ proposed: {
+ method: "COUNTER",
+ attendee: {
+ params: { cn: "Attendee", partstat: "NEEDS-ACTION", role: "REQ-PARTICIPANT" },
+ value: "attendee@example.net",
+ },
+ location: "Room 2",
+ dtstamp: "20150909T171048Z",
+ },
+ },
+ expected: {
+ result: {
+ descr: "This is a counterproposal not based on the latest event update.",
+ type: "NOTLATESTUPDATE",
+ },
+ differences: { location: { proposed: "Room 2", original: "Room 1" } },
+ },
+ },
+ {
+ name: "Proposed change based on a meanwhile reschuled invitation",
+ input: {
+ proposed: {
+ method: "COUNTER",
+ attendee: {
+ params: { cn: "Attendee", partstat: "NEEDS-ACTION", role: "REQ-PARTICIPANT" },
+ value: "attendee@example.net",
+ },
+ location: "Room 2",
+ sequence: "0",
+ dtstamp: "20150909T182048Z",
+ },
+ },
+ expected: {
+ result: {
+ descr: "This is a counterproposal to an already rescheduled event.",
+ type: "OUTDATED",
+ },
+ differences: { location: { proposed: "Room 2", original: "Room 1" } },
+ },
+ },
+ {
+ name: "Proposed change for an later sequence of the event",
+ input: {
+ proposed: {
+ method: "COUNTER",
+ attendee: {
+ params: { cn: "Attendee", partstat: "NEEDS-ACTION", role: "REQ-PARTICIPANT" },
+ value: "attendee@example.net",
+ },
+ location: "Room 2",
+ sequence: "2",
+ dtstamp: "20150909T182048Z",
+ },
+ },
+ expected: {
+ result: {
+ descr: "Invalid sequence number in counterproposal.",
+ type: "ERROR",
+ },
+ differences: {},
+ },
+ },
+ {
+ name: "Proposal to a different event",
+ input: {
+ proposed: {
+ method: "COUNTER",
+ uid: "cb189fdc-0000-0000-0000-31a08802249d",
+ attendee: {
+ params: { cn: "Attendee", partstat: "NEEDS-ACTION", role: "REQ-PARTICIPANT" },
+ value: "attendee@example.net",
+ },
+ location: "Room 2",
+ dtstamp: "20150909T182048Z",
+ },
+ },
+ expected: {
+ result: {
+ descr: "Mismatch of uid or organizer in counterproposal.",
+ type: "ERROR",
+ },
+ differences: {},
+ },
+ },
+ {
+ name: "Proposal with a different organizer",
+ input: {
+ proposed: {
+ method: "COUNTER",
+ organizer: {
+ params: { rsvp: "TRUE", cn: "Organizer", partstat: "ACCEPTED", role: "CHAIR" },
+ value: "organizer2@example.net",
+ },
+ attendee: {
+ params: { cn: "Attendee", partstat: "NEEDS-ACTION", role: "REQ-PARTICIPANT" },
+ value: "attendee@example.net",
+ },
+ dtstamp: "20150909T182048Z",
+ },
+ },
+ expected: {
+ result: {
+ descr: "Mismatch of uid or organizer in counterproposal.",
+ type: "ERROR",
+ },
+ differences: {},
+ },
+ },
+ {
+ name: "Counterproposal without any difference",
+ input: {
+ proposed: { method: "COUNTER" },
+ },
+ expected: {
+ result: {
+ descr: "No difference in counterproposal detected.",
+ type: "NODIFF",
+ },
+ differences: {},
+ },
+ },
+ ];
+ /* eslint-enable object-curly-newline */
+
+ let getItem = function (aProperties) {
+ let item = getIcs(aProperties);
+ return createEventFromIcalString(item);
+ };
+
+ let formatDt = function (aDateTime) {
+ if (!aDateTime) {
+ return null;
+ }
+ let datetime = cal.dtz.formatter.formatDateTime(aDateTime);
+ return datetime + " " + aDateTime.timezone.displayName;
+ };
+
+ for (let test of data) {
+ info(`testing ${test.name}`);
+ let existingItem = getItem();
+ let proposedItem = getItem(test.input.proposed);
+ let parsed = cal.invitation.parseCounter(proposedItem, existingItem);
+
+ equal(parsed.result.type, test.expected.result.type, `(test ${test.name}: result.type)`);
+ equal(parsed.result.descr, test.expected.result.descr, `(test ${test.name}: result.descr)`);
+ let parsedProps = [];
+ let additionalProps = [];
+ let missingProps = [];
+ parsed.differences.forEach(aDiff => {
+ let prop = aDiff.property.toLowerCase();
+ if (prop in test.expected.differences) {
+ let { proposed, original } = test.expected.differences[prop];
+ let foundProposed = aDiff.proposed;
+ let foundOriginal = aDiff.original;
+ if (["dtstart", "dtend"].includes(prop)) {
+ foundProposed = formatDt(foundProposed);
+ foundOriginal = formatDt(foundOriginal);
+ ok(foundProposed, `(test ${test.name}: have proposed time value for ${prop})`);
+ ok(foundOriginal, `(test ${test.name}: have original time value for ${prop})`);
+ }
+
+ if (proposed instanceof RegExp) {
+ ok(
+ proposed.test(foundProposed),
+ `(test ${test.name}: proposed "${foundProposed}" for ${prop} matches expected regex)`
+ );
+ } else {
+ equal(
+ foundProposed,
+ proposed,
+ `(test ${test.name}: proposed for ${prop} matches expected)`
+ );
+ }
+
+ if (original instanceof RegExp) {
+ ok(
+ original.test(foundOriginal),
+ `(test ${test.name}: original "${foundOriginal}" for ${prop} matches expected regex)`
+ );
+ } else {
+ equal(
+ foundOriginal,
+ original,
+ `(test ${test.name}: original for ${prop} matches expected)`
+ );
+ }
+
+ parsedProps.push(prop);
+ } else {
+ additionalProps.push(prop);
+ }
+ });
+ for (let prop in test.expected.differences) {
+ if (!parsedProps.includes(prop)) {
+ missingProps.push(prop);
+ }
+ }
+ ok(
+ additionalProps.length == 0,
+ `(test ${test.name}: should be no additional properties: ${additionalProps})`
+ );
+ ok(
+ missingProps.length == 0,
+ `(test ${test.name}: should be no missing properties: ${missingProps})`
+ );
+ }
+});