diff --git a/calendar/test/CalDAVServer.jsm b/calendar/test/CalDAVServer.jsm new file mode 100644 index 0000000000..08901c0741 --- /dev/null +++ b/calendar/test/CalDAVServer.jsm @@ -0,0 +1,438 @@ +/* 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/. */ + +const EXPORTED_SYMBOLS = ["CalDAVServer"]; + +Cu.importGlobalProperties(["crypto"]); + +const PREFIX_BINDINGS = { + c: "urn:ietf:params:xml:ns:caldav", + cs: "http://calendarserver.org/ns/", + d: "DAV:", +}; +const NAMESPACE_STRING = Object.entries(PREFIX_BINDINGS) + .map(([prefix, url]) => `xmlns:${prefix}="${url}"`) + .join(" "); + +const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); +const { CommonUtils } = ChromeUtils.import("resource://services-common/utils.js"); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var CalDAVServer = { + items: new Map(), + deletedItems: new Map(), + changeCount: 0, + server: null, + isOpen: false, + + open(username, password) { + this.server = new HttpServer(); + this.server.start(-1); + this.isOpen = true; + + this.username = username; + this.password = password; + this.server.registerPathHandler("/ping", this.ping); + + this.reset(); + }, + + reset() { + this.items.clear(); + this.deletedItems.clear(); + this.changeCount = 0; + this.resetHandlers(); + }, + + resetHandlers() { + this.server.registerPathHandler(this.path, this.directoryHandler.bind(this)); + this.server.registerPrefixHandler(this.path, this.itemHandler.bind(this)); + }, + + close() { + if (!this.isOpen) { + return Promise.resolve(); + } + return new Promise(resolve => + this.server.stop({ + onStopped: () => { + this.isOpen = false; + resolve(); + }, + }) + ); + }, + + get origin() { + return `http://localhost:${this.server.identity.primaryPort}`; + }, + + get path() { + return "/calendars/me/test/"; + }, + + get url() { + return `${this.origin}${this.path}`; + }, + + get altPath() { + return "/addressbooks/me/default/"; + }, + + get altURL() { + return `${this.origin}${this.altPath}`; + }, + + checkAuth(request, response) { + if (!this.username || !this.password) { + return true; + } + if (!request.hasHeader("Authorization")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + let value = request.getHeader("Authorization"); + if (!value.startsWith("Basic ")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + let [username, password] = atob(value.substring(6)).split(":"); + if (username != this.username || password != this.password) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + return true; + }, + + ping(request, response) { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/plain"); + response.write("pong"); + }, + + /** Handle any requests to the calendar itself. */ + + directoryHandler(request, response) { + if (!this.checkAuth(request, response)) { + return; + } + + if (request.method == "OPTIONS") { + response.setStatusLine("1.1", 204, "No Content"); + return; + } + + let input = new DOMParser().parseFromString( + CommonUtils.readBytesFromInputStream(request.bodyInputStream), + "text/xml" + ); + + switch (input.documentElement.localName) { + case "calendar-query": + Assert.equal(request.method, "REPORT"); + Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.c); + this.calendarQuery(input, response); + return; + case "calendar-multiget": + Assert.equal(request.method, "REPORT"); + Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.c); + this.calendarMultiGet(input, response); + return; + case "propfind": + Assert.equal(request.method, "PROPFIND"); + Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.d); + this.propFind(input, request.hasHeader("Depth") ? request.getHeader("Depth") : 0, response); + return; + case "sync-collection": + Assert.equal(request.method, "REPORT"); + Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.d); + this.syncCollection(input, response); + return; + } + + Assert.report(true, undefined, undefined, "Should not have reached here"); + response.setStatusLine("1.1", 404, "Not Found"); + response.setHeader("Content-Type", "text/plain"); + response.write(`No handler found for <${input.documentElement.localName}>`); + }, + + calendarQuery(input, response) { + let propNames = this._inputProps(input); + let output = ``; + for (let [href, item] of this.items) { + output += this._itemResponse(href, item, propNames); + } + output += ``; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write(output.replace(/>\s+<")); + }, + + async calendarMultiGet(input, response) { + let propNames = this._inputProps(input); + let output = ``; + for (let href of input.querySelectorAll("href")) { + href = href.textContent; + let item = this.items.get(href); + if (item) { + output += this._itemResponse(href, item, propNames); + } + } + output += ``; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write(output.replace(/>\s+<")); + }, + + propFind(input, depth, response) { + let propNames = this._inputProps(input); + + let propValues = { + "d:resourcetype": "", + "d:owner": "/principals/me/", + "d:current-user-principal": "/principals/me/", + "d:supported-report-set": + "", + "c:supported-calendar-component-set": "", + "d:getcontenttype": "text/calendar; charset=utf-8", + + "c:calendar-home-set": `/calendars/me/`, + "c:calendar-user-address-set": `mailto:me@invalid`, + "c:schedule-inbox-url": `/calendars/me/inbox/`, + "c:schedule-outbox-url": `/calendars/me/outbox/`, + "cs:getctag": this.changeCount, + "d:getetag": this.changeCount, + }; + + let output = ` + + ${this.path} + ${this._outputProps(propNames, propValues)} + `; + if (depth == 1) { + for (let [href, item] of this.items) { + output += this._itemResponse(href, item, propNames); + } + } + output += ``; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write(output.replace(/>\s+<")); + }, + + syncCollection(input, response) { + let token = input.querySelector("sync-token").textContent.replace(/\D/g, ""); + let propNames = this._inputProps(input); + + let output = ``; + for (let [href, item] of this.items) { + if (item.changed > token) { + output += this._itemResponse(href, item, propNames); + } + } + for (let [href, deleted] of this.deletedItems) { + if (deleted > token) { + output += ` + HTTP/1.1 404 Not Found + ${href} + + + HTTP/1.1 418 I'm a teapot + + `; + } + } + output += `http://mochi.test/sync/${this.changeCount} + `; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write(output.replace(/>\s+<")); + }, + + _itemResponse(href, item, propNames) { + let propValues = { + "c:calendar-data": item.ics, + "d:getetag": item.etag, + "d:getcontenttype": "text/calendar; charset=utf-8; component=VEVENT", + }; + + let outString = ` + ${href} + ${this._outputProps(propNames, propValues)} + `; + return outString; + }, + + _inputProps(input) { + let props = input.querySelectorAll("prop > *"); + let propNames = []; + + for (let p of props) { + Assert.equal(p.childElementCount, 0); + switch (p.localName) { + case "calendar-home-set": + case "calendar-user-address-set": + case "schedule-inbox-URL": + case "schedule-outbox-URL": + case "supported-calendar-component-set": + case "calendar-data": + Assert.equal(p.namespaceURI, PREFIX_BINDINGS.c); + propNames.push(`c:${p.localName}`); + break; + case "getctag": + Assert.equal(p.namespaceURI, PREFIX_BINDINGS.cs); + propNames.push(`cs:${p.localName}`); + break; + case "getetag": + case "owner": + case "current-user-principal": + case "supported-report-set": + case "displayname": + case "resourcetype": + case "sync-token": + case "getcontenttype": + Assert.equal(p.namespaceURI, PREFIX_BINDINGS.d); + propNames.push(`d:${p.localName}`); + break; + default: + Assert.report(true, undefined, undefined, `Unknown property requested: ${p.nodeName}`); + break; + } + } + + return propNames; + }, + + _outputProps(propNames, propValues) { + let output = ""; + + let found = []; + let notFound = []; + for (let p of propNames) { + if (p in propValues) { + found.push(`<${p}>${propValues[p]}`); + } else { + notFound.push(`<${p}/>`); + } + } + + if (found.length > 0) { + output += ` + + ${found.join("\n")} + + HTTP/1.1 200 OK + `; + } + if (notFound.length > 0) { + output += ` + + ${notFound.join("\n")} + + HTTP/1.1 404 Not Found + `; + } + + return output; + }, + + /** Handle any requests to calendar items. */ + + itemHandler(request, response) { + if (!this.checkAuth(request, response)) { + return; + } + + if (!/\/[\w-]+\.ics$/.test(request.path)) { + response.setStatusLine("1.1", 404, "Not Found"); + response.setHeader("Content-Type", "text/plain"); + response.write(`Item not found at ${request.path}`); + return; + } + + switch (request.method) { + case "GET": + this.getItem(request, response); + return; + case "PUT": + this.putItem(request, response); + return; + case "DELETE": + this.deleteItem(request, response); + return; + } + + Assert.report(true, undefined, undefined, "Should not have reached here"); + response.setStatusLine("1.1", 405, "Method Not Allowed"); + response.setHeader("Content-Type", "text/plain"); + response.write(`Method not allowed: ${request.method}`); + }, + + async getItem(request, response) { + let item = this.items.get(request.path); + if (!item) { + response.setStatusLine("1.1", 404, "Not Found"); + response.setHeader("Content-Type", "text/plain"); + response.write(`Item not found at ${request.path}`); + return; + } + + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/calendar"); + response.setHeader("ETag", item.etag); + response.write(item.ics); + }, + + async putItem(request, response) { + if (request.hasHeader("If-Match")) { + let item = this.items.get(request.path); + if (!item || item.etag != request.getHeader("If-Match")) { + response.setStatusLine("1.1", 412, "Precondition Failed"); + return; + } + } + + response.processAsync(); + + let ics = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + await this.putItemInternal(request.path, ics); + response.setStatusLine("1.1", 204, "No Content"); + + response.finish(); + }, + + async putItemInternal(name, ics) { + if (!name.startsWith("/")) { + name = this.path + name; + } + + let hash = await crypto.subtle.digest("sha-1", new TextEncoder().encode(ics)); + let etag = Array.from(new Uint8Array(hash), c => c.toString(16).padStart(2, "0")).join(""); + this.items.set(name, { etag, ics, changed: ++this.changeCount }); + this.deletedItems.delete(name); + }, + + deleteItem(request, response) { + this.deleteItemInternal(request.path); + response.setStatusLine("1.1", 204, "No Content"); + }, + + deleteItemInternal(name) { + if (!name.startsWith("/")) { + name = this.path + name; + } + this.items.delete(name); + this.deletedItems.set(name, ++this.changeCount); + }, +}; diff --git a/calendar/test/ICSServer.jsm b/calendar/test/ICSServer.jsm new file mode 100644 index 0000000000..1893839a12 --- /dev/null +++ b/calendar/test/ICSServer.jsm @@ -0,0 +1,155 @@ +/* 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/. */ + +const EXPORTED_SYMBOLS = ["ICSServer"]; + +Cu.importGlobalProperties(["crypto"]); + +const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); +const { CommonUtils } = ChromeUtils.import("resource://services-common/utils.js"); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var ICSServer = { + server: null, + isOpen: false, + + ics: "", + etag: "", + open(username, password) { + this.server = new HttpServer(); + this.server.start(-1); + this.isOpen = true; + + this.username = username; + this.password = password; + this.server.registerPathHandler("/ping", this.ping); + this.server.registerPathHandler(this.path, this.handleICS.bind(this)); + + this.reset(); + }, + + reset() { + this.ics = ""; + this.etag = ""; + }, + + close() { + if (!this.isOpen) { + return Promise.resolve(); + } + return new Promise(resolve => + this.server.stop({ + onStopped: () => { + this.isOpen = false; + resolve(); + }, + }) + ); + }, + + get origin() { + return `http://localhost:${this.server.identity.primaryPort}`; + }, + + get path() { + return "/test.ics"; + }, + + get url() { + return `${this.origin}${this.path}`; + }, + + get altPath() { + return "/addressbooks/me/default/"; + }, + + get altURL() { + return `${this.origin}${this.altPath}`; + }, + + checkAuth(request, response) { + if (!this.username || !this.password) { + return true; + } + if (!request.hasHeader("Authorization")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + let value = request.getHeader("Authorization"); + if (!value.startsWith("Basic ")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + let [username, password] = atob(value.substring(6)).split(":"); + if (username != this.username || password != this.password) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + return true; + }, + + ping(request, response) { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/plain"); + response.write("pong"); + }, + + handleICS(request, response) { + if (!this.checkAuth(request, response)) { + return; + } + + switch (request.method) { + case "HEAD": + this.headICS(request, response); + return; + case "GET": + this.getICS(request, response); + return; + case "PUT": + this.putICS(request, response); + return; + } + + Assert.report(true, undefined, undefined, "Should not have reached here"); + response.setStatusLine("1.1", 405, "Method Not Allowed"); + response.setHeader("Content-Type", "text/plain"); + response.write(`Method not allowed: ${request.method}`); + }, + + headICS(request, response) { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/calendar"); + response.setHeader("ETag", this.etag); + }, + + getICS(request, response) { + this.headICS(request, response); + response.write(this.ics); + }, + + async putICS(request, response) { + response.processAsync(); + + await this.putICSInternal(CommonUtils.readBytesFromInputStream(request.bodyInputStream)); + + response.setStatusLine("1.1", 204, "No Content"); + response.setHeader("ETag", this.etag); + + response.finish(); + }, + + async putICSInternal(ics) { + this.ics = ics; + + let hash = await crypto.subtle.digest("sha-1", new TextEncoder().encode(this.ics)); + this.etag = Array.from(new Uint8Array(hash), c => c.toString(16).padStart(2, "0")).join(""); + }, +}; diff --git a/calendar/test/moz.build b/calendar/test/moz.build index d5273a3ef7..58793547e6 100644 --- a/calendar/test/moz.build +++ b/calendar/test/moz.build @@ -15,6 +15,11 @@ BROWSER_CHROME_MANIFESTS += [ "browser/views/browser.ini", ] +TESTING_JS_MODULES += [ + "CalDAVServer.jsm", + "ICSServer.jsm", +] + TESTING_JS_MODULES.mozmill += [ "modules/CalendarTestUtils.jsm", "modules/CalendarUtils.jsm", @@ -22,6 +27,7 @@ TESTING_JS_MODULES.mozmill += [ ] XPCSHELL_TESTS_MANIFESTS += [ + "unit/providers/xpcshell.ini", "unit/xpcshell-icaljs.ini", "unit/xpcshell-libical.ini", ] diff --git a/calendar/test/unit/providers/head.js b/calendar/test/unit/providers/head.js new file mode 100644 index 0000000000..3d77769cbe --- /dev/null +++ b/calendar/test/unit/providers/head.js @@ -0,0 +1,167 @@ +/* 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 { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/mozmill/CalendarTestUtils.jsm" +); +var { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm"); +var { PromiseUtils } = ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm"); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// The tests in this directory each do the same thing, with slight variations as needed for each +// calendar provider. The core of the test lives in this file and the tests call it when ready. + +let manager = cal.getCalendarManager(); + +do_get_profile(); +add_task(async () => { + await new Promise(resolve => manager.startup({ onResult: resolve })); + await new Promise(resolve => cal.getTimezoneService().startup({ onResult: resolve })); + manager.addCalendarObserver(calendarObserver); +}); + +let calendarObserver = { + QueryInterface: ChromeUtils.generateQI(["calIObserver"]), + + /* calIObserver */ + + _batchCount: 0, + _batchRequired: true, + onStartBatch() { + info(`onStartBatch ${++this._batchCount}`); + Assert.equal(this._batchCount, 1, "onStartBatch must not occur in a batch"); + }, + onEndBatch() { + info(`onEndBatch ${this._batchCount--}`); + Assert.equal(this._batchCount, 0, "onEndBatch must occur in a batch"); + }, + onLoad(calendar) { + info(`onLoad ${calendar.id}`); + Assert.equal(this._batchCount, 0, "onLoad must not occur in a batch"); + if (this._onLoadPromise) { + this._onLoadPromise.resolve(); + } + }, + onAddItem(item) { + info(`onAddItem ${item.calendar.id} ${item.id}`); + if (this._batchRequired) { + Assert.equal(this._batchCount, 1, "onAddItem must occur in a batch"); + } + if (this._onAddItemPromise) { + this._onAddItemPromise.resolve(); + } + }, + onModifyItem(newItem, oldItem) { + info(`onModifyItem ${newItem.calendar.id} ${newItem.id}`); + if (this._batchRequired) { + Assert.equal(this._batchCount, 1, "onModifyItem must occur in a batch"); + } + if (this._onModifyItemPromise) { + this._onModifyItemPromise.resolve(); + } + }, + onDeleteItem(deletedItem) { + info(`onDeleteItem ${deletedItem.calendar.id} ${deletedItem.id}`); + if (this._onDeleteItemPromise) { + this._onDeleteItemPromise.resolve(); + } + }, + onError(calendar, errNo, message) {}, + onPropertyChanged(calendar, name, value, oldValue) {}, + onPropertyDeleting(calendar, name) {}, +}; + +/** + * Create and register a calendar. + * + * @param {string} type - The calendar provider to use. + * @param {string} url - URL of the server. + * @param {boolean} useCache - Should this calendar have offline storage? + * @returns {calICalendar} + */ +function createCalendar(type, url, useCache) { + let calendar = manager.createCalendar(type, Services.io.newURI(url)); + calendar.name = type + (useCache ? " with cache" : " without cache"); + calendar.id = cal.getUUID(); + calendar.setProperty("cache.enabled", useCache); + + manager.registerCalendar(calendar); + calendar = manager.getCalendarById(calendar.id); + return calendar; +} + +/** + * Wraps calICalendar's getItem method in a Promise. + * + * @param {calICalendar} calendar + * @param {string} uid + * @returns {Promise} - resolves to calIItemBase or null + */ +function getItem(calendar, uid) { + return new Promise(resolve => { + calendar.getItem(uid, { + _item: null, + onGetResult(c, status, itemType, detail, items) { + this._item = items[0]; + }, + onOperationComplete() { + resolve(this._item); + }, + }); + }); +} + +/** + * Creates an event and adds it to the given calendar. + * + * @param {calICalendar} calendar + * @returns {calIEvent} + */ +async function runAddItem(calendar) { + let event = new CalEvent(); + event.id = "6b7dd6f6-d6f0-4e93-a953-bb5473c4c47a"; + event.title = "New event"; + event.startDate = cal.createDateTime("20200303T205500Z"); + event.endDate = cal.createDateTime("20200303T210200Z"); + + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onModifyItemPromise = PromiseUtils.defer(); + calendar.addItem(event, null); + await Promise.any([ + calendarObserver._onAddItemPromise.promise, + calendarObserver._onModifyItemPromise.promise, + ]); + + return event; +} + +/** + * Modifies the event from runAddItem. + * + * @param {calICalendar} calendar + */ +async function runModifyItem(calendar) { + let event = await getItem(calendar, "6b7dd6f6-d6f0-4e93-a953-bb5473c4c47a"); + + let clone = event.clone(); + clone.title = "Modified event"; + + calendarObserver._onModifyItemPromise = PromiseUtils.defer(); + calendar.modifyItem(clone, event, null); + await calendarObserver._onModifyItemPromise.promise; +} + +/** + * Deletes the event from runAddItem. + * + * @param {calICalendar} calendar + */ +async function runDeleteItem(calendar) { + let event = await getItem(calendar, "6b7dd6f6-d6f0-4e93-a953-bb5473c4c47a"); + + calendarObserver._onDeleteItemPromise = PromiseUtils.defer(); + calendar.deleteItem(event, null); + await calendarObserver._onDeleteItemPromise.promise; +} diff --git a/calendar/test/unit/providers/test_caldavCalendar_cached.js b/calendar/test/unit/providers/test_caldavCalendar_cached.js new file mode 100644 index 0000000000..bb0f211c47 --- /dev/null +++ b/calendar/test/unit/providers/test_caldavCalendar_cached.js @@ -0,0 +1,46 @@ +/* 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 { CalDAVServer } = ChromeUtils.import("resource://testing-common/CalDAVServer.jsm"); + +CalDAVServer.open(); +CalDAVServer.putItemInternal( + "testfile.ics", + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:5a9fa76c-93f3-4ad8-9f00-9e52aedd2821 + SUMMARY:exists before time + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` +); +registerCleanupFunction(() => CalDAVServer.close()); + +add_task(async function() { + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("caldav", CalDAVServer.url, true); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(await getItem(calendar, "5a9fa76c-93f3-4ad8-9f00-9e52aedd2821")); + + info("creating the item"); + calendarObserver._batchRequired = true; + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runAddItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("modifying the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runModifyItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("deleting the item"); + await runDeleteItem(calendar); +}); diff --git a/calendar/test/unit/providers/test_caldavCalendar_uncached.js b/calendar/test/unit/providers/test_caldavCalendar_uncached.js new file mode 100644 index 0000000000..927e366dab --- /dev/null +++ b/calendar/test/unit/providers/test_caldavCalendar_uncached.js @@ -0,0 +1,46 @@ +/* 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 { CalDAVServer } = ChromeUtils.import("resource://testing-common/CalDAVServer.jsm"); + +CalDAVServer.open(); +CalDAVServer.putItemInternal( + "testfile.ics", + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:5a9fa76c-93f3-4ad8-9f00-9e52aedd2821 + SUMMARY:exists before time + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` +); +registerCleanupFunction(() => CalDAVServer.close()); + +add_task(async function() { + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("caldav", CalDAVServer.url, false); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(await getItem(calendar, "5a9fa76c-93f3-4ad8-9f00-9e52aedd2821")); + + info("creating the item"); + calendarObserver._batchRequired = true; + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runAddItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("modifying the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runModifyItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("deleting the item"); + await runDeleteItem(calendar); +}); diff --git a/calendar/test/unit/providers/test_icsCalendar_cached.js b/calendar/test/unit/providers/test_icsCalendar_cached.js new file mode 100644 index 0000000000..b0d5b02b39 --- /dev/null +++ b/calendar/test/unit/providers/test_icsCalendar_cached.js @@ -0,0 +1,53 @@ +/* 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 { ICSServer } = ChromeUtils.import("resource://testing-common/ICSServer.jsm"); + +ICSServer.open(); +ICSServer.putICSInternal( + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:5a9fa76c-93f3-4ad8-9f00-9e52aedd2821 + SUMMARY:exists before time + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` +); +registerCleanupFunction(() => ICSServer.close()); + +add_task(async function() { + // TODO: item notifications from a cached ICS calendar occur outside of batches. + // This isn't fatal but it shouldn't happen. Side-effects include alarms firing + // twice - once from onAddItem then again at onLoad. + // + // Remove the next line when this is fixed. + calendarObserver._batchRequired = false; + + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("ics", ICSServer.url, true); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(await getItem(calendar, "5a9fa76c-93f3-4ad8-9f00-9e52aedd2821")); + + info("creating the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runAddItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("modifying the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runModifyItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("deleting the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runDeleteItem(calendar); + await calendarObserver._onLoadPromise.promise; +}); diff --git a/calendar/test/unit/providers/test_icsCalendar_uncached.js b/calendar/test/unit/providers/test_icsCalendar_uncached.js new file mode 100644 index 0000000000..06e5cdddcb --- /dev/null +++ b/calendar/test/unit/providers/test_icsCalendar_uncached.js @@ -0,0 +1,46 @@ +/* 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 { ICSServer } = ChromeUtils.import("resource://testing-common/ICSServer.jsm"); + +ICSServer.open(); +ICSServer.putICSInternal( + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:5a9fa76c-93f3-4ad8-9f00-9e52aedd2821 + SUMMARY:exists before time + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` +); +registerCleanupFunction(() => ICSServer.close()); + +add_task(async function() { + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("ics", ICSServer.url, false); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(await getItem(calendar, "5a9fa76c-93f3-4ad8-9f00-9e52aedd2821")); + + info("creating the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runAddItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("modifying the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runModifyItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("deleting the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runDeleteItem(calendar); + await calendarObserver._onLoadPromise.promise; +}); diff --git a/calendar/test/unit/providers/test_storageCalendar.js b/calendar/test/unit/providers/test_storageCalendar.js new file mode 100644 index 0000000000..dbe85b541f --- /dev/null +++ b/calendar/test/unit/providers/test_storageCalendar.js @@ -0,0 +1,17 @@ +/* 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/. */ + +add_task(async function() { + let calendar = createCalendar("storage", "moz-storage-calendar://"); + + info("creating the item"); + calendarObserver._batchRequired = false; + await runAddItem(calendar); + + info("modifying the item"); + await runModifyItem(calendar); + + info("deleting the item"); + await runDeleteItem(calendar); +}); diff --git a/calendar/test/unit/providers/xpcshell.ini b/calendar/test/unit/providers/xpcshell.ini new file mode 100644 index 0000000000..2bc88eefe4 --- /dev/null +++ b/calendar/test/unit/providers/xpcshell.ini @@ -0,0 +1,10 @@ +[default] +head = head.js +prefs = + calendar.timezone.local=UTC + +[test_caldavCalendar_cached.js] +[test_caldavCalendar_uncached.js] +[test_icsCalendar_cached.js] +[test_icsCalendar_uncached.js] +[test_storageCalendar.js]