Bug 1691885 - Test notifications from CalDAV and ICS calendars. r=mkmelin
This tests each combination of calendar provider and offline storage setting to ensure they behave consistently. To do this I've added mock CalDAV and ICS servers (forked from CardDAVServer.jsm). Differential Revision: https://phabricator.services.mozilla.com/D109272 --HG-- rename : mailnews/addrbook/test/CardDAVServer.jsm => calendar/test/CalDAVServer.jsm rename : mailnews/addrbook/test/CardDAVServer.jsm => calendar/test/ICSServer.jsm extra : moz-landing-system : lando
This commit is contained in:
Родитель
71b3d5f7bf
Коммит
650d6b9be3
|
@ -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 = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
|
||||||
|
for (let [href, item] of this.items) {
|
||||||
|
output += this._itemResponse(href, item, propNames);
|
||||||
|
}
|
||||||
|
output += `</multistatus>`;
|
||||||
|
|
||||||
|
response.setStatusLine("1.1", 207, "Multi-Status");
|
||||||
|
response.setHeader("Content-Type", "text/xml");
|
||||||
|
response.write(output.replace(/>\s+</g, "><"));
|
||||||
|
},
|
||||||
|
|
||||||
|
async calendarMultiGet(input, response) {
|
||||||
|
let propNames = this._inputProps(input);
|
||||||
|
let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
|
||||||
|
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 += `</multistatus>`;
|
||||||
|
|
||||||
|
response.setStatusLine("1.1", 207, "Multi-Status");
|
||||||
|
response.setHeader("Content-Type", "text/xml");
|
||||||
|
response.write(output.replace(/>\s+</g, "><"));
|
||||||
|
},
|
||||||
|
|
||||||
|
propFind(input, depth, response) {
|
||||||
|
let propNames = this._inputProps(input);
|
||||||
|
|
||||||
|
let propValues = {
|
||||||
|
"d:resourcetype": "<d:collection/><c:calendar/>",
|
||||||
|
"d:owner": "/principals/me/",
|
||||||
|
"d:current-user-principal": "/principals/me/",
|
||||||
|
"d:supported-report-set":
|
||||||
|
"<d:supported-report><d:report><c:calendar-multiget/></d:report></d:supported-report>",
|
||||||
|
"c:supported-calendar-component-set": "",
|
||||||
|
"d:getcontenttype": "text/calendar; charset=utf-8",
|
||||||
|
|
||||||
|
"c:calendar-home-set": `<d:href>/calendars/me/</d:href>`,
|
||||||
|
"c:calendar-user-address-set": `<d:href preferred="1">mailto:me@invalid</d:href>`,
|
||||||
|
"c:schedule-inbox-url": `<d:href>/calendars/me/inbox/</d:href>`,
|
||||||
|
"c:schedule-outbox-url": `<d:href>/calendars/me/outbox/</d:href>`,
|
||||||
|
"cs:getctag": this.changeCount,
|
||||||
|
"d:getetag": this.changeCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
|
||||||
|
<response>
|
||||||
|
<href>${this.path}</href>
|
||||||
|
${this._outputProps(propNames, propValues)}
|
||||||
|
</response>`;
|
||||||
|
if (depth == 1) {
|
||||||
|
for (let [href, item] of this.items) {
|
||||||
|
output += this._itemResponse(href, item, propNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += `</multistatus>`;
|
||||||
|
|
||||||
|
response.setStatusLine("1.1", 207, "Multi-Status");
|
||||||
|
response.setHeader("Content-Type", "text/xml");
|
||||||
|
response.write(output.replace(/>\s+</g, "><"));
|
||||||
|
},
|
||||||
|
|
||||||
|
syncCollection(input, response) {
|
||||||
|
let token = input.querySelector("sync-token").textContent.replace(/\D/g, "");
|
||||||
|
let propNames = this._inputProps(input);
|
||||||
|
|
||||||
|
let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
|
||||||
|
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 += `<response>
|
||||||
|
<status>HTTP/1.1 404 Not Found</status>
|
||||||
|
<href>${href}</href>
|
||||||
|
<propstat>
|
||||||
|
<prop/>
|
||||||
|
<status>HTTP/1.1 418 I'm a teapot</status>
|
||||||
|
</propstat>
|
||||||
|
</response>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += `<sync-token>http://mochi.test/sync/${this.changeCount}</sync-token>
|
||||||
|
</multistatus>`;
|
||||||
|
|
||||||
|
response.setStatusLine("1.1", 207, "Multi-Status");
|
||||||
|
response.setHeader("Content-Type", "text/xml");
|
||||||
|
response.write(output.replace(/>\s+</g, "><"));
|
||||||
|
},
|
||||||
|
|
||||||
|
_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 = `<response>
|
||||||
|
<href>${href}</href>
|
||||||
|
${this._outputProps(propNames, propValues)}
|
||||||
|
</response>`;
|
||||||
|
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]}</${p}>`);
|
||||||
|
} else {
|
||||||
|
notFound.push(`<${p}/>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found.length > 0) {
|
||||||
|
output += `<propstat>
|
||||||
|
<prop>
|
||||||
|
${found.join("\n")}
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>`;
|
||||||
|
}
|
||||||
|
if (notFound.length > 0) {
|
||||||
|
output += `<propstat>
|
||||||
|
<prop>
|
||||||
|
${notFound.join("\n")}
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 404 Not Found</status>
|
||||||
|
</propstat>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
|
@ -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("");
|
||||||
|
},
|
||||||
|
};
|
|
@ -15,6 +15,11 @@ BROWSER_CHROME_MANIFESTS += [
|
||||||
"browser/views/browser.ini",
|
"browser/views/browser.ini",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
TESTING_JS_MODULES += [
|
||||||
|
"CalDAVServer.jsm",
|
||||||
|
"ICSServer.jsm",
|
||||||
|
]
|
||||||
|
|
||||||
TESTING_JS_MODULES.mozmill += [
|
TESTING_JS_MODULES.mozmill += [
|
||||||
"modules/CalendarTestUtils.jsm",
|
"modules/CalendarTestUtils.jsm",
|
||||||
"modules/CalendarUtils.jsm",
|
"modules/CalendarUtils.jsm",
|
||||||
|
@ -22,6 +27,7 @@ TESTING_JS_MODULES.mozmill += [
|
||||||
]
|
]
|
||||||
|
|
||||||
XPCSHELL_TESTS_MANIFESTS += [
|
XPCSHELL_TESTS_MANIFESTS += [
|
||||||
|
"unit/providers/xpcshell.ini",
|
||||||
"unit/xpcshell-icaljs.ini",
|
"unit/xpcshell-icaljs.ini",
|
||||||
"unit/xpcshell-libical.ini",
|
"unit/xpcshell-libical.ini",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
|
@ -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;
|
||||||
|
});
|
|
@ -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;
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
|
@ -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]
|
Загрузка…
Ссылка в новой задаче