summaryrefslogtreecommitdiffstats
path: root/comm/calendar/test/unit/test_caldav_requests.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/test/unit/test_caldav_requests.js')
-rw-r--r--comm/calendar/test/unit/test_caldav_requests.js970
1 files changed, 970 insertions, 0 deletions
diff --git a/comm/calendar/test/unit/test_caldav_requests.js b/comm/calendar/test/unit/test_caldav_requests.js
new file mode 100644
index 0000000000..2f46c09a8d
--- /dev/null
+++ b/comm/calendar/test/unit/test_caldav_requests.js
@@ -0,0 +1,970 @@
+/* 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 { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+var { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+var {
+ CalDavGenericRequest,
+ CalDavItemRequest,
+ CalDavDeleteItemRequest,
+ CalDavPropfindRequest,
+ CalDavHeaderRequest,
+ CalDavPrincipalPropertySearchRequest,
+ CalDavOutboxRequest,
+ CalDavFreeBusyRequest,
+} = ChromeUtils.import("resource:///modules/caldav/CalDavRequest.jsm");
+var { CalDavWebDavSyncHandler } = ChromeUtils.import(
+ "resource:///modules/caldav/CalDavRequestHandlers.jsm"
+);
+
+var { CalDavSession } = ChromeUtils.import("resource:///modules/caldav/CalDavSession.jsm");
+var { CalDavXmlns } = ChromeUtils.import("resource:///modules/caldav/CalDavUtils.jsm");
+var { Preferences } = ChromeUtils.importESModule("resource://gre/modules/Preferences.sys.mjs");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalEvent: "resource:///modules/CalEvent.jsm",
+});
+
+class LowerMap extends Map {
+ get(key) {
+ return super.get(key.toLowerCase());
+ }
+ set(key, value) {
+ return super.set(key.toLowerCase(), value);
+ }
+}
+
+var gServer;
+
+var MockConflictPrompt = {
+ _origFunc: null,
+ overwrite: false,
+ register() {
+ if (!this._origFunc) {
+ this._origFunc = cal.provider.promptOverwrite;
+ cal.provider.promptOverwrite = (aMode, aItem) => {
+ return this.overwrite;
+ };
+ }
+ },
+
+ unregister() {
+ if (this._origFunc) {
+ cal.provider.promptOverwrite = this._origFunc;
+ this._origFunc = null;
+ }
+ },
+};
+
+class MockAlertsService {
+ QueryInterface = ChromeUtils.generateQI(["nsIAlertsService"]);
+ showAlert() {}
+}
+
+function replaceAlertsService() {
+ let originalAlertsServiceCID = MockRegistrar.register(
+ "@mozilla.org/alerts-service;1",
+ MockAlertsService
+ );
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(originalAlertsServiceCID);
+ });
+}
+
+var gMockCalendar = {
+ name: "xpcshell",
+ makeUri(insert, base) {
+ return base;
+ },
+ verboseLogging() {
+ return true;
+ },
+ ensureEncodedPath(x) {
+ return x;
+ },
+ ensureDecodedPath(x) {
+ return x;
+ },
+ startBatch() {},
+ endBatch() {},
+ addTargetCalendarItem() {},
+ finalizeUpdatedItems() {},
+ mHrefIndex: [],
+};
+gMockCalendar.superCalendar = gMockCalendar;
+
+class CalDavServer {
+ constructor(calendarId) {
+ this.server = new HttpServer();
+ this.calendarId = calendarId;
+ this.session = new CalDavSession(this.calendarId, "xpcshell");
+ this.serverRequests = {};
+
+ this.server.registerPrefixHandler(
+ "/principals/",
+ this.router.bind(this, this.principals.bind(this))
+ );
+ this.server.registerPrefixHandler(
+ "/calendars/",
+ this.router.bind(this, this.calendars.bind(this))
+ );
+ this.server.registerPrefixHandler(
+ "/requests/",
+ this.router.bind(this, this.requests.bind(this))
+ );
+ }
+
+ start() {
+ this.server.start(-1);
+ registerCleanupFunction(() => this.server.stop(() => {}));
+ }
+
+ reset() {
+ this.serverRequests = {};
+ }
+
+ uri(path) {
+ let base = Services.io.newURI(`http://localhost:${this.server.identity.primaryPort}/`);
+ return Services.io.newURI(path, null, base);
+ }
+
+ router(nextHandler, request, response) {
+ try {
+ let method = request.method;
+ let parameters = new Map(request.queryString.split("&").map(part => part.split("=", 2)));
+ let available = request.bodyInputStream.available();
+ let body =
+ available > 0 ? NetUtil.readInputStreamToString(request.bodyInputStream, available) : null;
+
+ let headers = new LowerMap();
+
+ let headerIterator = function* (enumerator) {
+ while (enumerator.hasMoreElements()) {
+ yield enumerator.getNext().QueryInterface(Ci.nsISupportsString);
+ }
+ };
+
+ for (let hdr of headerIterator(request.headers)) {
+ headers.set(hdr.data, request.getHeader(hdr.data));
+ }
+
+ return nextHandler(request, response, method, headers, parameters, body);
+ } catch (e) {
+ info("Server Error: " + e.fileName + ":" + e.lineNumber + ": " + e + "\n");
+ return null;
+ }
+ }
+
+ resetClient(client) {
+ MockConflictPrompt.unregister();
+ cal.manager.unregisterCalendar(client);
+ }
+
+ waitForLoad(aCalendar) {
+ return new Promise((resolve, reject) => {
+ let observer = cal.createAdapter(Ci.calIObserver, {
+ onLoad() {
+ let uncached = aCalendar.wrappedJSObject.mUncachedCalendar.wrappedJSObject;
+ aCalendar.removeObserver(observer);
+
+ if (Components.isSuccessCode(uncached._lastStatus)) {
+ resolve(aCalendar);
+ } else {
+ reject(uncached._lastMessage);
+ }
+ },
+ });
+ aCalendar.addObserver(observer);
+ });
+ }
+
+ getClient() {
+ let uri = this.uri("/calendars/xpcshell/events");
+ let client = cal.manager.createCalendar("caldav", uri);
+ let uclient = client.wrappedJSObject;
+ client.name = "xpcshell";
+ client.setProperty("cache.enabled", true);
+
+ // Make sure we catch the last error message in case sync fails
+ monkeyPatch(uclient, "replayChangesOn", (protofunc, aListener) => {
+ protofunc({
+ onResult(operation, detail) {
+ uclient._lastStatus = operation.status;
+ uclient._lastMessage = detail;
+ aListener.onResult(operation, detail);
+ },
+ });
+ });
+
+ cal.manager.registerCalendar(client);
+
+ let cachedCalendar = cal.manager.getCalendarById(client.id);
+ return this.waitForLoad(cachedCalendar);
+ }
+
+ principals(request, response, method, headers, parameters, body) {
+ this.serverRequests.principals = { method, headers, parameters, body };
+
+ if (method == "REPORT" && request.path == "/principals/") {
+ response.setHeader("Content-Type", "application/xml");
+ response.write(dedent`
+ <?xml version="1.0" encoding="utf-8" ?>
+ <D:multistatus xmlns:D="DAV:" xmlns:B="http://BigCorp.com/ns/">
+ <D:response>
+ <D:href>http://www.example.com/users/jdoe</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:displayname>John Doe</D:displayname>
+ <B:department>Widget Sales</B:department>
+ <B:phone>234-4567</B:phone>
+ <B:office>209</B:office>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ <D:propstat>
+ <D:prop>
+ <B:salary/>
+ </D:prop>
+ <D:status>HTTP/1.1 403 Forbidden</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>http://www.example.com/users/zsmith</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:displayname>Zygdoebert Smith</D:displayname>
+ <B:department>Gadget Sales</B:department>
+ <B:phone>234-7654</B:phone>
+ <B:office>114</B:office>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ <D:propstat>
+ <D:prop>
+ <B:salary/>
+ </D:prop>
+ <D:status>HTTP/1.1 403 Forbidden</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>
+ `);
+ response.setStatusLine(null, 207, "Multistatus");
+ } else if (method == "PROPFIND" && request.path == "/principals/xpcshell/user/") {
+ response.setHeader("Content-Type", "application/xml");
+ response.write(dedent`
+ <?xml version="1.0" encoding="utf-8"?>
+ <D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:response>
+ <D:href>${this.uri("/principals/xpcshell/user").spec}</D:href>
+ <D:propstat>
+ <D:prop>
+ <C:calendar-home-set>
+ <D:href>${this.uri("/calendars/xpcshell/user/").spec}</D:href>
+ </C:calendar-home-set>
+ <C:calendar-user-address-set>
+ <D:href>mailto:xpcshell@example.com</D:href>
+ </C:calendar-user-address-set>
+ <C:schedule-inbox-URL>
+ <D:href>${this.uri("/calendars/xpcshell/inbox").spec}/</D:href>
+ </C:schedule-inbox-URL>
+ <C:schedule-outbox-URL>
+ <D:href>${this.uri("/calendars/xpcshell/outbox").spec}</D:href>
+ </C:schedule-outbox-URL>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>
+ `);
+ response.setStatusLine(null, 207, "Multistatus");
+ }
+ }
+
+ calendars(request, response, method, headers, parameters, body) {
+ this.serverRequests.calendars = { method, headers, parameters, body };
+
+ if (
+ method == "PROPFIND" &&
+ request.path.startsWith("/calendars/xpcshell/events") &&
+ headers.get("depth") == 0
+ ) {
+ response.setHeader("Content-Type", "application/xml");
+ response.write(dedent`
+ <?xml version="1.0" encoding="utf-8" ?>
+ <D:multistatus ${CalDavXmlns("D", "C", "CS")} xmlns:R="http://www.foo.bar/boxschema/">
+ <D:response>
+ <D:href>${request.path}</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:resourcetype>
+ <D:collection/>
+ <C:calendar/>
+ </D:resourcetype>
+ <R:plain-text-prop>hello, world</R:plain-text-prop>
+ <D:principal-collection-set>
+ <D:href>${this.uri("/principals/").spec}</D:href>
+ <D:href>${this.uri("/principals/subthing/").spec}</D:href>
+ </D:principal-collection-set>
+ <D:current-user-principal>
+ <D:href>${this.uri("/principals/xpcshell/user").spec}</D:href>
+ </D:current-user-principal>
+ <D:supported-report-set>
+ <D:supported-report>
+ <D:report>
+ <D:principal-property-search/>
+ </D:report>
+ </D:supported-report>
+ <D:supported-report>
+ <D:report>
+ <C:calendar-multiget/>
+ </D:report>
+ </D:supported-report>
+ <D:supported-report>
+ <D:report>
+ <D:sync-collection/>
+ </D:report>
+ </D:supported-report>
+ </D:supported-report-set>
+ <C:supported-calendar-component-set>
+ <C:comp name="VEVENT"/>
+ <C:comp name="VTODO"/>
+ </C:supported-calendar-component-set>
+ <C:schedule-inbox-URL>
+ <D:href>${this.uri("/calendars/xpcshell/inbox").spec}</D:href>
+ </C:schedule-inbox-URL>
+ <C:schedule-outbox-URL>
+ ${this.uri("/calendars/xpcshell/outbox").spec}
+ </C:schedule-outbox-URL>
+ <CS:getctag>1413647159-1007960</CS:getctag>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ <D:propstat>
+ <D:prop>
+ <R:obscure-thing-not-found/>
+ </D:prop>
+ <D:status>HTTP/1.1 404 Not Found</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>
+ `);
+ response.setStatusLine(null, 207, "Multistatus");
+ } else if (method == "POST" && request.path == "/calendars/xpcshell/outbox") {
+ response.setHeader("Content-Type", "application/xml");
+ response.write(dedent`
+ <?xml version="1.0" encoding="utf-8" ?>
+ <C:schedule-response ${CalDavXmlns("D", "C")}>
+ <D:response>
+ <D:href>mailto:recipient1@example.com</D:href>
+ <D:request-status>2.0;Success</D:request-status>
+ </D:response>
+ <D:response>
+ <D:href>mailto:recipient2@example.com</D:href>
+ <D:request-status>2.0;Success</D:request-status>
+ </D:response>
+ </C:schedule-response>
+ `);
+ response.setStatusLine(null, 200, "OK");
+ } else if (method == "POST" && request.path == "/calendars/xpcshell/outbox2") {
+ response.setHeader("Content-Type", "application/xml");
+ response.write(dedent`
+ <?xml version="1.0" encoding="utf-8" ?>
+ <C:schedule-response ${CalDavXmlns("D", "C")}>
+ <D:response>
+ <D:recipient>
+ <D:href>mailto:recipient1@example.com</D:href>
+ </D:recipient>
+ <D:request-status>2.0;Success</D:request-status>
+ <C:calendar-data>
+ BEGIN:VCALENDAR
+ PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+ VERSION:2.0
+ METHOD:REQUEST
+ BEGIN:VFREEBUSY
+ DTSTART;VALUE=DATE:20180102
+ DTEND;VALUE=DATE:20180126
+ ORGANIZER:mailto:xpcshell@example.com
+ ATTENDEE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL:mail
+ to:recipient@example.com
+ FREEBUSY;FBTYPE=FREE:20180103T010101Z/20180117T010101Z
+ FREEBUSY;FBTYPE=BUSY:20180118T010101Z/P7D
+ END:VFREEBUSY
+ END:VCALENDAR
+ </C:calendar-data>
+ </D:response>
+ </C:schedule-response>
+ `);
+ response.setStatusLine(null, 200, "OK");
+ } else if (method == "OPTIONS" && request.path == "/calendars/xpcshell/") {
+ response.setHeader(
+ "DAV",
+ "1, 2, 3, access-control, extended-mkcol, resource-sharing, calendar-access, calendar-auto-schedule, calendar-query-extended, calendar-availability, calendarserver-sharing, inbox-availability"
+ );
+ response.setStatusLine(null, 200, "OK");
+ } else if (method == "REPORT" && request.path == "/calendars/xpcshell/events/") {
+ response.setHeader("Content-Type", "application/xml");
+ let bodydom = cal.xml.parseString(body);
+ let report = bodydom.documentElement.localName;
+ let eventName = String.fromCharCode(...new TextEncoder().encode("イベント"));
+ if (report == "sync-collection") {
+ response.write(dedent`
+ <?xml version="1.0" encoding="utf-8" ?>
+ <D:multistatus ${CalDavXmlns("D")}>
+ <D:response>
+ <D:href>${this.uri("/calendars/xpcshell/events/test.ics").spec}</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getcontenttype>text/calendar; charset=utf-8; component=VEVENT</D:getcontenttype>
+ <D:getetag>"2decee6ffb701583398996bfbdacb8eec53edf94"</D:getetag>
+ <D:displayname>${eventName}</D:displayname>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>
+ `);
+ } else if (report == "calendar-multiget") {
+ let event = new CalEvent();
+ event.title = "会議";
+ event.startDate = cal.dtz.now();
+ event.endDate = cal.dtz.now();
+ let icalString = String.fromCharCode(...new TextEncoder().encode(event.icalString));
+ response.write(dedent`
+ <?xml version="1.0" encoding="utf-8"?>
+ <D:multistatus ${CalDavXmlns("D", "C")}>
+ <D:response>
+ <D:href>${this.uri("/calendars/xpcshell/events/test.ics").spec}</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>"2decee6ffb701583398996bfbdacb8eec53edf94"</D:getetag>
+ <C:calendar-data>${icalString}</C:calendar-data>
+ </D:prop>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>
+ `);
+ }
+ response.setStatusLine(null, 207, "Multistatus");
+ } else {
+ console.log("XXX: " + method, request.path, [...headers.entries()]);
+ }
+ }
+
+ requests(request, response, method, headers, parameters, body) {
+ // ["", "requests", "generic"] := /requests/generic
+ let parts = request.path.split("/");
+ let id = parts[2];
+ let status = parseInt(parts[3] || "", 10) || 200;
+
+ if (id == "redirected") {
+ response.setHeader("Location", "/requests/redirected-target", false);
+ status = 302;
+ } else if (id == "dav") {
+ response.setHeader("DAV", "1, calendar-schedule, calendar-auto-schedule");
+ }
+
+ this.serverRequests[id] = { method, headers, parameters, body };
+
+ for (let [hdr, value] of headers.entries()) {
+ response.setHeader(hdr, "response-" + value, false);
+ }
+
+ response.setHeader("Content-Type", "application/xml");
+ response.write(`<response id="${id}">xpc</response>`);
+ response.setStatusLine(null, status, null);
+ }
+}
+
+function run_test() {
+ Preferences.set("calendar.debug.log", true);
+ Preferences.set("calendar.debug.log.verbose", true);
+ cal.console.maxLogLevel = "debug";
+ replaceAlertsService();
+
+ // TODO: make do_calendar_startup to work with this test and replace the startup code here
+ do_get_profile();
+ do_test_pending();
+
+ cal.manager.startup({
+ onResult() {
+ gServer = new CalDavServer("xpcshell@example.com");
+ gServer.start();
+ cal.timezoneService.startup({
+ onResult() {
+ run_next_test();
+ do_test_finished();
+ },
+ });
+ },
+ });
+}
+
+add_task(async function test_caldav_session() {
+ gServer.reset();
+
+ let prepared = 0;
+ let redirected = 0;
+ let completed = 0;
+ let restart = false;
+
+ gServer.session.authAdapters.localhost = {
+ async prepareRequest(aChannel) {
+ prepared++;
+ },
+
+ async prepareRedirect(aOldChannel, aNewChannel) {
+ redirected++;
+ },
+
+ async completeRequest(aResponse) {
+ completed++;
+ if (restart) {
+ restart = false;
+ return CalDavSession.RESTART_REQUEST;
+ }
+ return null;
+ },
+ };
+
+ // First a simple request
+ let uri = gServer.uri("/requests/session");
+ let request = new CalDavGenericRequest(gServer.session, gMockCalendar, "HEAD", uri);
+ await request.commit();
+
+ equal(prepared, 1);
+ equal(redirected, 0);
+ equal(completed, 1);
+
+ // Now a redirect
+ prepared = redirected = completed = 0;
+
+ uri = gServer.uri("/requests/redirected");
+ request = new CalDavGenericRequest(gServer.session, gMockCalendar, "HEAD", uri);
+ await request.commit();
+
+ equal(prepared, 1);
+ equal(redirected, 1);
+ equal(completed, 1);
+
+ // Now with restarting the request
+ prepared = redirected = completed = 0;
+ restart = true;
+
+ uri = gServer.uri("/requests/redirected");
+ request = new CalDavGenericRequest(gServer.session, gMockCalendar, "HEAD", uri);
+ await request.commit();
+
+ equal(prepared, 2);
+ equal(redirected, 2);
+ equal(completed, 2);
+});
+
+/**
+ * This test covers both GenericRequest and the base class CalDavRequestBase/CalDavResponseBase
+ */
+add_task(async function test_generic_request() {
+ gServer.reset();
+ let uri = gServer.uri("/requests/generic");
+ let headers = { "X-Hdr": "exists" };
+ let request = new CalDavGenericRequest(
+ gServer.session,
+ gMockCalendar,
+ "PUT",
+ uri,
+ headers,
+ "<body>xpc</body>",
+ "text/plain"
+ );
+
+ strictEqual(request.uri.spec, uri.spec);
+ strictEqual(request.session.id, gServer.session.id);
+ strictEqual(request.calendar, gMockCalendar);
+ strictEqual(request.uploadData, "<body>xpc</body>");
+ strictEqual(request.contentType, "text/plain");
+ strictEqual(request.response, null);
+ strictEqual(request.getHeader("X-Hdr"), null); // Only works after commit
+
+ let response = await request.commit();
+
+ ok(!!request.response);
+ equal(request.getHeader("X-Hdr"), "exists");
+
+ equal(response.uri.spec, uri.spec);
+ ok(!response.redirected);
+ equal(response.status, 200);
+ equal(response.statusCategory, 2);
+ ok(response.ok);
+ ok(!response.clientError);
+ ok(!response.conflict);
+ ok(!response.notFound);
+ ok(!response.serverError);
+ equal(response.text, '<response id="generic">xpc</response>');
+ equal(response.xml.documentElement.localName, "response");
+ equal(response.getHeader("X-Hdr"), "response-exists");
+
+ let serverResult = gServer.serverRequests.generic;
+
+ equal(serverResult.method, "PUT");
+ equal(serverResult.headers.get("x-hdr"), "exists");
+ equal(serverResult.headers.get("content-type"), "text/plain");
+ equal(serverResult.body, "<body>xpc</body>");
+});
+
+add_task(async function test_generic_redirected_request() {
+ gServer.reset();
+ let uri = gServer.uri("/requests/redirected");
+ let headers = {
+ Depth: 1,
+ Originator: "o",
+ Recipient: "r",
+ "If-None-Match": "*",
+ "If-Match": "123",
+ };
+ let request = new CalDavGenericRequest(
+ gServer.session,
+ gMockCalendar,
+ "PUT",
+ uri,
+ headers,
+ "<body>xpc</body>",
+ "text/plain"
+ );
+
+ let response = await request.commit();
+
+ ok(response.redirected);
+ equal(response.status, 200);
+ equal(response.text, '<response id="redirected-target">xpc</response>');
+ equal(response.xml.documentElement.getAttribute("id"), "redirected-target");
+
+ ok(gServer.serverRequests.redirected);
+ ok(gServer.serverRequests["redirected-target"]);
+
+ let results = gServer.serverRequests.redirected;
+ equal(results.headers.get("Depth"), 1);
+ equal(results.headers.get("Originator"), "o");
+ equal(results.headers.get("Recipient"), "r");
+ equal(results.headers.get("If-None-Match"), "*");
+ equal(results.headers.get("If-Match"), "123");
+
+ results = gServer.serverRequests["redirected-target"];
+ equal(results.headers.get("Depth"), 1);
+ equal(results.headers.get("Originator"), "o");
+ equal(results.headers.get("Recipient"), "r");
+ equal(results.headers.get("If-None-Match"), "*");
+ equal(results.headers.get("If-Match"), "123");
+
+ equal(response.lastRedirectStatus, 302);
+});
+
+add_task(async function test_item_request() {
+ gServer.reset();
+ let uri = gServer.uri("/requests/item/201");
+ let icalString = "BEGIN:VEVENT\r\nUID:123\r\nEND:VEVENT";
+ let componentString = `BEGIN:VCALENDAR\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\nVERSION:2.0\r\n${icalString}\r\nEND:VCALENDAR\r\n`;
+ let request = new CalDavItemRequest(
+ gServer.session,
+ gMockCalendar,
+ uri,
+ new CalEvent(icalString),
+ "*"
+ );
+ let response = await request.commit();
+
+ equal(response.status, 201);
+ ok(response.ok);
+
+ let serverResult = gServer.serverRequests.item;
+
+ equal(serverResult.method, "PUT");
+ equal(serverResult.body, componentString);
+ equal(serverResult.headers.get("If-None-Match"), "*");
+ ok(!serverResult.headers.has("If-Match"));
+
+ // Now the same with 204 No Content and an etag
+ gServer.reset();
+ uri = gServer.uri("/requests/item/204");
+ request = new CalDavItemRequest(
+ gServer.session,
+ gMockCalendar,
+ uri,
+ new CalEvent(icalString),
+ "123123"
+ );
+ response = await request.commit();
+
+ equal(response.status, 204);
+ ok(response.ok);
+
+ serverResult = gServer.serverRequests.item;
+
+ equal(serverResult.method, "PUT");
+ equal(serverResult.body, componentString);
+ equal(serverResult.headers.get("If-Match"), "123123");
+ ok(!serverResult.headers.has("If-None-Match"));
+
+ // Now the same with 200 OK and no etag
+ gServer.reset();
+ uri = gServer.uri("/requests/item/200");
+ request = new CalDavItemRequest(gServer.session, gMockCalendar, uri, new CalEvent(icalString));
+ response = await request.commit();
+
+ equal(response.status, 200);
+ ok(response.ok);
+
+ serverResult = gServer.serverRequests.item;
+
+ equal(serverResult.method, "PUT");
+ equal(serverResult.body, componentString);
+ ok(!serverResult.headers.has("If-Match"));
+ ok(!serverResult.headers.has("If-None-Match"));
+});
+
+add_task(async function test_delete_item_request() {
+ gServer.reset();
+ let uri = gServer.uri("/requests/deleteitem");
+ let request = new CalDavDeleteItemRequest(gServer.session, gMockCalendar, uri, "*");
+
+ strictEqual(request.uploadData, null);
+ strictEqual(request.contentType, null);
+
+ let response = await request.commit();
+
+ equal(response.status, 200);
+ ok(response.ok);
+
+ let serverResult = gServer.serverRequests.deleteitem;
+
+ equal(serverResult.method, "DELETE");
+ equal(serverResult.headers.get("If-Match"), "*");
+ ok(!serverResult.headers.has("If-None-Match"));
+
+ // Now the same with no etag, and a (valid) 404 response
+ gServer.reset();
+ uri = gServer.uri("/requests/deleteitem/404");
+ request = new CalDavDeleteItemRequest(gServer.session, gMockCalendar, uri);
+ response = await request.commit();
+
+ equal(response.status, 404);
+ ok(response.ok);
+
+ serverResult = gServer.serverRequests.deleteitem;
+
+ equal(serverResult.method, "DELETE");
+ ok(!serverResult.headers.has("If-Match"));
+ ok(!serverResult.headers.has("If-None-Match"));
+});
+
+add_task(async function test_propfind_request() {
+ gServer.reset();
+ let uri = gServer.uri("/calendars/xpcshell/events");
+ let props = [
+ "D:principal-collection-set",
+ "D:current-user-principal",
+ "D:supported-report-set",
+ "C:supported-calendar-component-set",
+ "C:schedule-inbox-URL",
+ "C:schedule-outbox-URL",
+ "R:obscure-thing-not-found",
+ ];
+ let request = new CalDavPropfindRequest(gServer.session, gMockCalendar, uri, props);
+ let response = await request.commit();
+
+ equal(response.status, 207);
+ ok(response.ok);
+
+ let results = gServer.serverRequests.calendars;
+
+ ok(
+ results.body.match(/<D:prop>\s*<D:principal-collection-set\/>\s*<D:current-user-principal\/>/)
+ );
+
+ equal(Object.keys(response.data).length, 1);
+ ok(!!response.data[uri.filePath]);
+ ok(!!response.firstProps);
+
+ let resprops = response.firstProps;
+
+ deepEqual(resprops["D:principal-collection-set"], [
+ gServer.uri("/principals/").spec,
+ gServer.uri("/principals/subthing/").spec,
+ ]);
+ equal(resprops["D:current-user-principal"], gServer.uri("/principals/xpcshell/user").spec);
+
+ deepEqual(
+ [...resprops["D:supported-report-set"].values()],
+ ["D:principal-property-search", "C:calendar-multiget", "D:sync-collection"]
+ );
+
+ deepEqual([...resprops["C:supported-calendar-component-set"].values()], ["VEVENT", "VTODO"]);
+ equal(resprops["C:schedule-inbox-URL"], gServer.uri("/calendars/xpcshell/inbox").spec);
+ equal(resprops["C:schedule-outbox-URL"], gServer.uri("/calendars/xpcshell/outbox").spec);
+ strictEqual(resprops["R:obscure-thing-not-found"], null);
+ equal(resprops["R:plain-text-prop"], "hello, world");
+});
+
+add_task(async function test_davheader_request() {
+ gServer.reset();
+ let uri = gServer.uri("/requests/dav");
+ let request = new CalDavHeaderRequest(gServer.session, gMockCalendar, uri);
+ let response = await request.commit();
+
+ let serverResult = gServer.serverRequests.dav;
+
+ equal(serverResult.method, "OPTIONS");
+ deepEqual([...response.features], ["calendar-schedule", "calendar-auto-schedule"]);
+ strictEqual(response.version, 1);
+});
+
+add_task(async function test_propsearch_request() {
+ gServer.reset();
+ let uri = gServer.uri("/principals/");
+ let props = ["D:displayname", "B:department", "B:phone", "B:office"];
+ let request = new CalDavPrincipalPropertySearchRequest(
+ gServer.session,
+ gMockCalendar,
+ uri,
+ "doE",
+ "D:displayname",
+ props
+ );
+ let response = await request.commit();
+
+ equal(response.status, 207);
+ ok(response.ok);
+
+ equal(response.data["http://www.example.com/users/jdoe"]["D:displayname"], "John Doe");
+
+ ok(gServer.serverRequests.principals.body.includes("<D:match>doE</D:match>"));
+ ok(gServer.serverRequests.principals.body.match(/<D:prop>\s*<D:displayname\/>\s*<\/D:prop>/));
+ ok(
+ gServer.serverRequests.principals.body.match(/<D:prop>\s*<D:displayname\/>\s*<B:department\/>/)
+ );
+});
+
+add_task(async function test_outbox_request() {
+ gServer.reset();
+ let icalString = "BEGIN:VEVENT\r\nUID:123\r\nEND:VEVENT";
+ let uri = gServer.uri("/calendars/xpcshell/outbox");
+ let request = new CalDavOutboxRequest(
+ gServer.session,
+ gMockCalendar,
+ uri,
+ "xpcshell@example.com",
+ ["recipient1@example.com", "recipient2@example.com"],
+ "REPLY",
+ new CalEvent(icalString)
+ );
+ let response = await request.commit();
+
+ equal(response.status, 200);
+ ok(response.ok);
+
+ let results = gServer.serverRequests.calendars;
+
+ ok(results.body.includes("METHOD:REPLY"));
+ equal(results.method, "POST");
+ equal(results.headers.get("Originator"), "xpcshell@example.com");
+ equal(results.headers.get("Recipient"), "recipient1@example.com, recipient2@example.com");
+});
+
+add_task(async function test_freebusy_request() {
+ gServer.reset();
+ let uri = gServer.uri("/calendars/xpcshell/outbox2");
+ let request = new CalDavFreeBusyRequest(
+ gServer.session,
+ gMockCalendar,
+ uri,
+ "mailto:xpcshell@example.com",
+ "mailto:recipient@example.com",
+ cal.createDateTime("20180101"),
+ cal.createDateTime("20180201")
+ );
+
+ let response = await request.commit();
+
+ equal(response.status, 200);
+ ok(response.ok);
+
+ let results = gServer.serverRequests.calendars;
+ equal(
+ ics_unfoldline(
+ results.body
+ .replace(/\r\n/g, "\n")
+ .replace(/(UID|DTSTAMP):[^\n]+\n/g, "")
+ .trim()
+ ),
+ dedent`
+ BEGIN:VCALENDAR
+ PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+ VERSION:2.0
+ METHOD:REQUEST
+ BEGIN:VFREEBUSY
+ DTSTART;VALUE=DATE:20180101
+ DTEND;VALUE=DATE:20180201
+ ORGANIZER:mailto:xpcshell@example.com
+ ATTENDEE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL:mailto:recipient@example.com
+ END:VFREEBUSY
+ END:VCALENDAR
+ `
+ );
+ equal(results.method, "POST");
+ equal(results.headers.get("Content-Type"), "text/calendar; charset=utf-8");
+ equal(results.headers.get("Originator"), "mailto:xpcshell@example.com");
+ equal(results.headers.get("Recipient"), "mailto:recipient@example.com");
+
+ let first = response.firstRecipient;
+ equal(first.status, "2.0;Success");
+ deepEqual(
+ first.intervals.map(interval => interval.type),
+ ["UNKNOWN", "FREE", "BUSY", "UNKNOWN"]
+ );
+ deepEqual(
+ first.intervals.map(interval => interval.begin.icalString + ":" + interval.end.icalString),
+ [
+ "20180101:20180102",
+ "20180103T010101Z:20180117T010101Z",
+ "20180118T010101Z:20180125T010101Z",
+ "20180126:20180201",
+ ]
+ );
+});
+
+add_task(async function test_caldav_client() {
+ let client = await gServer.getClient();
+ let items = await client.getItemsAsArray(Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null);
+
+ equal(items.length, 1);
+ equal(items[0].title, "会議");
+});
+
+/**
+ * Test non-ASCII text in the XML response is parsed correctly in CalDavWebDavSyncHandler.
+ */
+add_task(async function test_caldav_sync() {
+ gServer.reset();
+ let uri = gServer.uri("/calendars/xpcshell/events/");
+ gMockCalendar.session = gServer.session;
+ let webDavSync = new CalDavWebDavSyncHandler(gMockCalendar, uri);
+ await webDavSync.doWebDAVSync();
+ ok(webDavSync.logXML.includes("イベント"), "Non-ASCII text should be parsed correctly");
+});
+
+add_task(function test_can_get_google_adapter() {
+ // Initialize a session with bogus values
+ const session = new CalDavSession("xpcshell@example.com", "xpcshell");
+
+ // We don't have a facility for actually testing our Google CalDAV requests,
+ // but we can at least verify that the adapter looks okay at a glance
+ equal(
+ session.authAdapters["apidata.googleusercontent.com"].authorizationEndpoint,
+ "https://accounts.google.com/o/oauth2/auth"
+ );
+});