Bug 1546606 - Refactor CalDAV request handling. r=pmorris

MozReview-Commit-ID: 81oND0JtEFK
This commit is contained in:
Philipp Kewisch 2020-04-09 13:51:09 +03:00
Родитель d7ac1094d1
Коммит e4cb801505
9 изменённых файлов: 3102 добавлений и 1424 удалений

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -6,8 +6,7 @@ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
var { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
var xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n';
var MIME_TEXT_XML = "text/xml; charset=utf-8";
ChromeUtils.import("resource:///modules/caldav/calDavRequest.jsm");
/**
* This is a handler for the etag request in calDavCalendar.js' getUpdatedItem.
@ -176,7 +175,9 @@ etagsHandler.prototype = {
if (this.calendar.verboseLogging()) {
this.logXML += aValue;
}
this.currentResponse[this.tag] += aValue;
if (this.tag) {
this.currentResponse[this.tag] += aValue;
}
},
startDocument() {
@ -312,7 +313,7 @@ webDavSyncHandler.prototype = {
doWebDAVSync() {
if (this.calendar.mDisabled) {
// check if maybe our calendar has become available
this.calendar.setupAuthentication(this.changeLogListener);
this.calendar.checkDavResourceType(this.changeLogListener);
return;
}
@ -323,7 +324,7 @@ webDavSyncHandler.prototype = {
}
let queryXml =
xmlHeader +
XML_HEADER +
'<sync-collection xmlns="DAV:">' +
syncTokenString +
"<sync-level>1</sync-level>" +
@ -339,32 +340,32 @@ webDavSyncHandler.prototype = {
cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
}
cal.LOG("CalDAV: webdav-sync Token: " + this.calendar.mWebdavSyncToken);
this.calendar.sendHttpRequest(
let request = new LegacySAXRequest(
this.calendar.session,
this.calendar,
requestUri,
queryXml,
MIME_TEXT_XML,
null,
this,
channel => {
// The depth header adheres to an older version of the webdav-sync
// spec and has been replaced by the <sync-level> tag above.
// Unfortunately some servers still depend on the depth header,
// therefore we send both (yuck).
channel.setRequestHeader("Depth", "1", false);
channel.requestMethod = "REPORT";
return this;
},
() => {
// Something went wrong with the OAuth token, notify failure
if (this.calendar.isCached && this.changeLogListener) {
this.changeLogListener.onResult(
{ status: Cr.NS_ERROR_NOT_AVAILABLE },
Cr.NS_ERROR_NOT_AVAILABLE
);
}
},
false
}
);
request.commit().catch(() => {
// Something went wrong with the OAuth token, notify failure
if (this.calendar.isCached && this.changeLogListener) {
this.changeLogListener.onResult(
{ status: Cr.NS_ERROR_NOT_AVAILABLE },
Cr.NS_ERROR_NOT_AVAILABLE
);
}
});
},
/**
@ -713,7 +714,7 @@ multigetSyncHandler.prototype = {
doMultiGet() {
if (this.calendar.mDisabled) {
// check if maybe our calendar has become available
this.calendar.setupAuthentication(this.changeLogListener);
this.calendar.checkDavResourceType(this.changeLogListener);
return;
}
@ -728,7 +729,7 @@ multigetSyncHandler.prototype = {
}
let queryXml =
xmlHeader +
XML_HEADER +
'<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">' +
"<D:prop>" +
"<D:getetag/>" +
@ -741,27 +742,28 @@ multigetSyncHandler.prototype = {
if (this.calendar.verboseLogging()) {
cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
}
this.calendar.sendHttpRequest(
let request = new LegacySAXRequest(
this.calendar.session,
this.calendar,
requestUri,
queryXml,
MIME_TEXT_XML,
null,
this,
channel => {
channel.requestMethod = "REPORT";
channel.setRequestHeader("Depth", "1", false);
return this;
},
() => {
// Something went wrong with the OAuth token, notify failure
if (this.calendar.isCached && this.changeLogListener) {
this.changeLogListener.onResult(
{ status: Cr.NS_ERROR_NOT_AVAILABLE },
Cr.NS_ERROR_NOT_AVAILABLE
);
}
},
false
}
);
request.commit().catch(() => {
// Something went wrong with the OAuth token, notify failure
if (this.calendar.isCached && this.changeLogListener) {
this.changeLogListener.onResult(
{ status: Cr.NS_ERROR_NOT_AVAILABLE },
Cr.NS_ERROR_NOT_AVAILABLE
);
}
});
},
/**
@ -859,8 +861,8 @@ multigetSyncHandler.prototype = {
/**
* @see nsISAXErrorHandler
*/
fatalError() {
cal.WARN("CalDAV: Fatal Error doing multiget for " + this.calendar.name);
fatalError(error) {
cal.WARN("CalDAV: Fatal Error doing multiget for " + this.calendar.name + ": " + error);
},
/**
@ -870,7 +872,9 @@ multigetSyncHandler.prototype = {
if (this.calendar.verboseLogging()) {
this.logXML += aValue;
}
this.currentResponse[this.tag] += aValue;
if (this.tag) {
this.currentResponse[this.tag] += aValue;
}
},
startDocument() {
@ -930,7 +934,6 @@ multigetSyncHandler.prototype = {
resp.status.indexOf(" 404") > 0
) {
if (this.calendar.mHrefIndex[resp.href]) {
this.changeCount++;
this.calendar.deleteTargetCalendarItem(resp.href);
} else {
cal.LOG("CalDAV: skipping unfound deleted item : " + resp.href);
@ -952,7 +955,6 @@ multigetSyncHandler.prototype = {
oldEtag = null;
}
if (!oldEtag || oldEtag != resp.getetag) {
this.changeCount++;
this.calendar.addTargetCalendarItem(
resp.href,
resp.calendardata,

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,345 @@
/* 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/. */
ChromeUtils.import("resource://gre/modules/Timer.jsm");
ChromeUtils.import("resource:///modules/OAuth2.jsm");
ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
/**
* Session and authentication tools for the caldav provider
*/
this.EXPORTED_SYMBOLS = ["CalDavSession"]; /* exported CalDavSession */
const OAUTH_GRACE_TIME = 30 * 1000;
/**
* Authentication provider for Google's OAuth.
*/
class CalDavGoogleOAuth extends OAuth2 {
/**
* Constructs a new Google OAuth autentication provider
*
* @param {String} sessionId The session id, used in the password manager
* @param {String} name The user-readable description of this session
*/
constructor(sessionId, name) {
super(OAUTH_BASE_URI, OAUTH_SCOPE, OAUTH_CLIENT_ID, OAUTH_HASH);
this.id = sessionId;
this.pwMgrId = "Google CalDAV v2";
this.requestWindowTitle = cal.l10n.getAnyString(
"global",
"commonDialogs",
"EnterUserPasswordFor2",
[name]
);
this.requestWindowFeatures = "chrome,private,centerscreen,width=430,height=600";
}
/**
* Returns true if the token has expired, or will expire within the grace time
*/
get tokenExpired() {
let now = new Date().getTime();
return this.tokenExpires - OAUTH_GRACE_TIME < now;
}
/**
* Retrieves the refresh token from the password manager. The token is cached.
*/
get refreshToken() {
if (!this._refreshToken) {
let pass = { value: null };
try {
let origin = "oauth:" + this.id;
cal.auth.passwordManagerGet(this.id, pass, origin, this.pwMgrId);
} catch (e) {
// User might have cancelled the master password prompt, thats ok
if (e.result != Cr.NS_ERROR_ABORT) {
throw e;
}
}
this._refreshToken = pass.value;
}
return this._refreshToken;
}
/**
* Saves the refresh token in the password manager
* @param {String} aVal The value to set
*/
set refreshToken(aVal) {
try {
let origin = "oauth:" + this.id;
if (aVal) {
cal.auth.passwordManagerSave(this.id, aVal, origin, this.pwMgrId);
} else {
cal.auth.passwordManagerRemove(this.id, origin, this.pwMgrId);
}
} catch (e) {
// User might have cancelled the master password prompt, thats ok
if (e.result != Cr.NS_ERROR_ABORT) {
throw e;
}
}
return (this._refreshToken = aVal);
}
/**
* Wait for the calendar window to appear.
*
* This is a workaround for bug 901329: If the calendar window isn't loaded yet the master
* password prompt will show just the buttons and possibly hang. If we postpone until the window
* is loaded, all is well.
*
* @return {Promise} A promise resolved without value when the window is loaded
*/
waitForCalendarWindow() {
return new Promise(resolve => {
// eslint-disable-next-line func-names, require-jsdoc
function postpone() {
let win = cal.window.getCalendarWindow();
if (!win || win.document.readyState != "complete") {
setTimeout(postpone, 0);
} else {
resolve();
}
}
setTimeout(postpone, 0);
});
}
/**
* Promisified version of |connect|, using all means necessary to gracefully display the
* authentication prompt.
*
* @param {Boolean} aWithUI If UI should be shown for authentication
* @param {Boolean} aRefresh Force refresh the token TODO default false
* @return {Promise} A promise resolved when the OAuth process is completed
*/
promiseConnect(aWithUI = true, aRefresh = true) {
return this.waitForCalendarWindow().then(() => {
return new Promise((resolve, reject) => {
let self = this;
let asyncprompter = Cc["@mozilla.org/messenger/msgAsyncPrompter;1"].getService(
Ci.nsIMsgAsyncPrompter
);
asyncprompter.queueAsyncAuthPrompt(this.id, false, {
onPromptStartAsync(callback) {
this.onPromptAuthAvailable(callback);
},
onPromptAuthAvailable(callback) {
self.connect(
() => {
if (callback) {
callback.onAuthResult(true);
}
resolve();
},
() => {
if (callback) {
callback.onAuthResult(false);
}
reject();
},
aWithUI,
aRefresh
);
},
onPromptCanceled: reject,
onPromptStart() {},
});
});
});
}
/**
* Prepare the given channel for an OAuth request
*
* @param {nsIChannel} aChannel The channel to prepare
*/
async prepareRequest(aChannel) {
if (!this.accessToken || this.tokenExpired) {
// The token has expired, we need to reauthenticate first
cal.LOG("CalDAV: OAuth token expired or empty, refreshing");
await this.promiseConnect();
}
let hdr = "Bearer " + this.accessToken;
aChannel.setRequestHeader("Authorization", hdr, false);
}
/**
* Prepare the redirect, copying the auth header to the new channel
*
* @param {nsIChannel} aOldChannel The old channel that is being redirected
* @param {nsIChannel} aNewChannel The new channel to prepare
*/
async prepareRedirect(aOldChannel, aNewChannel) {
try {
let hdrValue = aOldChannel.getRequestHeader("WWW-Authenticate");
if (hdrValue) {
aNewChannel.setRequestHeader("WWW-Authenticate", hdrValue, false);
}
} catch (e) {
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
// The header could possibly not be availible, ignore that
// case but throw otherwise
throw e;
}
}
}
/**
* Check for OAuth auth errors and restart the request without a token if necessary
*
* @param {CalDavResponse} aResponse The response to inspect for completion
* @return {Promise} A promise resolved when complete, with
* CalDavSession.RESTART_REQUEST or null
*/
async completeRequest(aResponse) {
// Check for OAuth errors
let wwwauth = aResponse.getHeader("WWW-Authenticate");
if (this.oauth && wwwauth && wwwauth.startsWith("Bearer") && wwwauth.includes("error=")) {
this.oauth.accessToken = null;
return CalDavSession.RESTART_REQUEST;
}
return null;
}
}
/**
* A session for the caldav provider. Two or more calendars can share a session if they have the
* same auth credentials.
*/
class CalDavSession {
QueryInterface(aIID) {
return cal.generateClassQI(this, aIID, [Ci.nsIInterfaceRequestor]);
}
/**
* Constant returned by |completeRequest| when the request should be restarted
* @return {Number} The constant
*/
static get RESTART_REQUEST() {
return 1;
}
/**
* Creates a new caldav session
*
* @param {String} aSessionId The session id, used in the password manager
* @param {String} aName The user-readable description of this session
*/
constructor(aSessionId, aName) {
this.id = aSessionId;
this.name = aName;
// There is only one right now, but for better separation this is ready for more oauth hosts
/* eslint-disable object-curly-newline */
this.authAdapters = {
"apidata.googleusercontent.com": new CalDavGoogleOAuth(aSessionId, aName),
};
/* eslint-enable object-curly-newline */
}
/**
* Implement nsIInterfaceRequestor. The base class has no extra interfaces, but a subclass of
* the session may.
*
* @param {nsIIDRef} aIID The IID of the interface being requested
* @return {?*} Either this object QI'd to the IID, or null.
* Components.returnCode is set accordingly.
*/
getInterface(aIID) {
try {
// Try to query the this object for the requested interface but don't
// throw if it fails since that borks the network code.
return this.QueryInterface(aIID);
} catch (e) {
Components.returnCode = e;
}
return null;
}
/**
* Calls the auth adapter for the given host in case it exists. This allows delegating auth
* preparation based on the host, e.g. for OAuth.
*
* @param {String} aHost The host to check the auth adapter for
* @param {String} aMethod The method to call
* @param {...*} aArgs Remaining args specific to the adapted method
* @return {*} Return value specific to the adapter method
*/
async _callAdapter(aHost, aMethod, ...aArgs) {
let adapter = this.authAdapters[aHost] || null;
if (adapter) {
return adapter[aMethod](...aArgs);
}
return null;
}
/**
* Prepare the channel for a request, e.g. setting custom authentication headers
*
* @param {nsIChannel} aChannel The channel to prepare
* @return {Promise} A promise resolved when the preparations are complete
*/
async prepareRequest(aChannel) {
return this._callAdapter(aChannel.URI.host, "prepareRequest", aChannel);
}
/**
* Prepare the given new channel for a redirect, e.g. copying headers.
*
* @param {nsIChannel} aOldChannel The old channel that is being redirected
* @param {nsIChannel} aNewChannel The new channel to prepare
* @return {Promise} A promise resolved when the preparations are complete
*/
async prepareRedirect(aOldChannel, aNewChannel) {
return this._callAdapter(aNewChannel.URI.host, "prepareRedirect", aOldChannel, aNewChannel);
}
/**
* Complete the request based on the results from the response. Allows restarting the session if
* |CalDavSession.RESTART_REQUEST| is returned.
*
* @param {CalDavResponse} aResponse The response to inspect for completion
* @return {Promise} A promise resolved when complete, with
* CalDavSession.RESTART_REQUEST or null
*/
async completeRequest(aResponse) {
return this._callAdapter(aResponse.request.uri.host, "completeRequest", aResponse);
}
}
// Before you spend time trying to find out what this means, please note that
// doing so and using the information WILL cause Google to revoke Lightning's
// privileges, which means not one Lightning user will be able to connect to
// Google Calendar via CalDAV. This will cause unhappy users all around which
// means that the Lightning developers will have to spend more time with user
// support, which means less time for features, releases and bugfixes. For a
// paid developer this would actually mean financial harm.
//
// Do you really want all of this to be your fault? Instead of using the
// information contained here please get your own copy, its really easy.
/* eslint-disable */
// prettier-ignore
(zqdx=>{zqdx["\x65\x76\x61\x6C"](zqdx["\x41\x72\x72\x61\x79"]["\x70\x72\x6F\x74"+
"\x6F\x74\x79\x70\x65"]["\x6D\x61\x70"]["\x63\x61\x6C\x6C"]("uijt/PBVUI`CBTF`VS"+
"J>#iuuqt;00bddpvout/hpphmf/dpn0p0#<uijt/PBVUI`TDPQF>#iuuqt;00xxx/hpphmfbqjt/dp"+
"n0bvui0dbmfoebs#<uijt/PBVUI`DMJFOU`JE>#831674:95649/bqqt/hpphmfvtfsdpoufou/dpn"+
"#<uijt/PBVUI`IBTI>#zVs7YVgyvsbguj7s8{1TTfJR#<",_=>zqdx["\x53\x74\x72\x69\x6E"+
"\x67"]["\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65"](_["\x63\x68\x61\x72"+
"\x43\x6F\x64\x65\x41\x74"](0)-1),this)[""+"\x6A\x6F\x69\x6E"](""))})["\x63\x61"+
"\x6C\x6C"]((this),Components["\x75\x74\x69\x6c\x73"]["\x67\x65\x74\x47\x6c\x6f"+
"\x62\x61\x6c\x46\x6f\x72\x4f\x62\x6a\x65\x63\x74"](this))
/* eslint-enable */

Просмотреть файл

@ -0,0 +1,108 @@
/* 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/. */
/**
* Various utility functions for the caldav provider
*/
/* exported xmlns, tagsToXmlns, caldavNSUnresolver, caldavNSResolver, caldavXPath,
* caldavXPathFirst */
this.EXPORTED_SYMBOLS = [
"xmlns",
"tagsToXmlns",
"caldavNSUnresolver",
"caldavNSResolver",
"caldavXPath",
"caldavXPathFirst",
];
/**
* Creates an xmlns string with the requested namespace prefixes
*
* @param {...String} aRequested The requested namespace prefixes
* @return {String} An xmlns string that can be inserted into xml documents
*/
function xmlns(...aRequested) {
let namespaces = [];
for (let namespace of aRequested) {
let nsUri = caldavNSResolver(namespace);
if (namespace) {
namespaces.push(`xmlns:${namespace}='${nsUri}'`);
}
}
return namespaces.join(" ");
}
/**
* Helper function to gather namespaces from QNames or namespace prefixes, plus a few extra for the
* remaining request.
*
* @param {...String} aTags Either QNames, or just namespace prefixes to be resolved.
* @return {String} The complete namespace string
*/
function tagsToXmlns(...aTags) {
let namespaces = new Set(aTags.map(tag => tag.split(":")[0]));
return xmlns(...namespaces.values());
}
/**
* Resolve the namespace URI to one of the prefixes used in our codebase
*
* @param {String} aNamespace The namespace URI to resolve
* @return {?String} The namespace prefix we use
*/
function caldavNSUnresolver(aNamespace) {
const prefixes = {
"http://apple.com/ns/ical/": "A",
"DAV:": "D",
"urn:ietf:params:xml:ns:caldav": "C",
"http://calendarserver.org/ns/": "CS",
};
return prefixes[aNamespace] || null;
}
/**
* Resolve the namespace URI from one of the prefixes used in our codebase
*
* @param {String} aPrefix The namespace prefix we use
* @return {?String} The namespace URI for the prefix
*/
function caldavNSResolver(aPrefix) {
/* eslint-disable id-length */
const namespaces = {
A: "http://apple.com/ns/ical/",
D: "DAV:",
C: "urn:ietf:params:xml:ns:caldav",
CS: "http://calendarserver.org/ns/",
};
/* eslint-enable id-length */
return namespaces[aPrefix] || null;
}
/**
* Run an xpath expression on the given node, using the caldav namespace resolver
*
* @param {Element} aNode The context node to search from
* @param {String} aExpr The XPath expression to search for
* @param {?XPathResult} aType (optional) Force a result type, must be an XPathResult constant
* @return {Element[]} Array of found elements
*/
function caldavXPath(aNode, aExpr, aType) {
return cal.xml.evalXPath(aNode, aExpr, caldavNSResolver, aType);
}
/**
* Run an xpath expression on the given node, using the caldav namespace resolver. Returns the first
* result.
*
* @param {Element} aNode The context node to search from
* @param {String} aExpr The XPath expression to search for
* @param {?XPathResult} aType (optional) Force a result type, must be an XPathResult constant
* @return {?Element} The found element, or null.
*/
function caldavXPathFirst(aNode, aExpr, aType) {
return cal.xml.evalXPathFirst(aNode, aExpr, caldavNSResolver, aType);
}

Просмотреть файл

@ -13,6 +13,12 @@ XPCOM_MANIFESTS += [
'components.conf',
]
EXTRA_JS_MODULES.caldav += [
'modules/calDavRequest.jsm',
'modules/calDavSession.jsm',
'modules/calDavUtils.jsm',
]
# These files go in components so they can be packaged correctly.
FINAL_TARGET_FILES.components += [
'calDavRequestHandlers.js',

Просмотреть файл

@ -196,26 +196,36 @@ function ics_unfoldline(aLine) {
*/
function dedent(strings, ...values) {
let parts = [];
// Perform variable interpolation
let minIndent = Infinity;
for (let [i, string] of strings.entries()) {
parts.push(string);
if (i < values.length) {
parts.push(values[i]);
let innerparts = string.split("\n");
if (i == 0) {
innerparts.shift();
}
if (i == strings.length - 1) {
innerparts.pop();
}
for (let [j, ip] of innerparts.entries()) {
let match = ip.match(/^(\s*)\S*/);
if (j != 0) {
minIndent = Math.min(minIndent, match[1].length);
}
}
parts.push(innerparts);
}
let lines = parts.join("").split("\n");
// The first and last line is empty as in above example.
lines.shift();
lines.pop();
let minIndent = lines.reduce((min, line) => {
let match = line.match(/^(\s*)\S*/);
return Math.min(min, match[1].length);
}, Infinity);
return lines.map(line => line.substr(minIndent)).join("\n");
return parts
.map((part, i) => {
return (
part
.map((line, j) => {
return j == 0 && i > 0 ? line : line.substr(minIndent);
})
.join("\n") + (i < values.length ? values[i] : "")
);
})
.join("");
}
/**
@ -280,3 +290,27 @@ function do_calendar_startup(callback) {
}
}
}
/**
* Monkey patch the function with the name x on obj and overwrite it with func.
* The first parameter of this function is the original function that can be
* called at any time.
*
* @param obj The object the function is on.
* @param name The string name of the function.
* @param func The function to monkey patch with.
*/
function monkeyPatch(obj, x, func) {
let old = obj[x];
obj[x] = function() {
let parent = old.bind(obj);
let args = Array.from(arguments);
args.unshift(parent);
try {
return func.apply(obj, args);
} catch (e) {
Cu.reportError(e);
throw e;
}
};
}

Просмотреть файл

@ -0,0 +1,890 @@
/* 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/. */
ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
ChromeUtils.import("resource://testing-common/httpd.js");
ChromeUtils.import("resource://testing-common/MockRegistrar.jsm");
ChromeUtils.import("resource:///modules/caldav/calDavSession.jsm");
ChromeUtils.import("resource:///modules/caldav/calDavRequest.jsm");
ChromeUtils.import("resource:///modules/caldav/calDavUtils.jsm");
Components.utils.importGlobalProperties(["URL"]);
class LowerMap extends Map {
get(key) {
return super.get(key.toLowerCase());
}
}
var gServer;
var MockConflictPrompt = {
_origFunc: null,
overwrite: false,
register: function() {
if (!this._origFunc) {
this._origFunc = cal.provider.promptOverwrite;
cal.provider.promptOverwrite = (aMode, aItem) => {
return this.overwrite;
};
}
},
unregister: function() {
if (this._origFunc) {
cal.provider.promptOverwrite = this._origFunc;
this._origFunc = null;
}
},
};
class MockAlertsService {
QueryInterface(aIID) {
return cal.generateClassQI(this, aIID, [Ci.nsIAlertsService]);
}
showAlertNotification() {}
}
function replaceAlertsService() {
let originalAlertsServiceCID = MockRegistrar.register(
"@mozilla.org/alerts-service;1",
MockAlertsService
);
registerCleanupFunction(() => {
MockRegistrar.unregister(originalAlertsServiceCID);
});
}
var gMockCalendar = { name: "xpcshell" };
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();
for (let hdr of XPCOMUtils.IterSimpleEnumerator(request.headers, Ci.nsISupportsString)) {
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.getCalendarManager().unregisterCalendar(client);
}
waitForLoad(aCalendar) {
return new Promise((resolve, reject) => {
let observer = cal.createAdapter(Components.interfaces.calIObserver, {
onLoad: function() {
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 calmgr = cal.getCalendarManager();
let client = calmgr.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: function(operation, detail) {
uclient._lastStatus = operation.status;
uclient._lastMessage = detail;
aListener.onResult(operation, detail);
},
});
});
calmgr.registerCalendar(client);
let cachedCalendar = calmgr.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 ${xmlns("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 ${xmlns("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 ${xmlns("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:20180103/20180117
FREEBUSY;FBTYPE=BUSY:20180118/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;
if (report == "sync-collection") {
response.write(dedent`
<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus ${xmlns("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:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>
`);
} else if (report == "calendar-multiget") {
let event = cal.createEvent();
event.startDate = cal.dtz.now();
event.endDate = cal.dtz.now();
response.write(dedent`
<?xml version="1.0" encoding="utf-8"?>
<D:multistatus ${xmlns("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>${event.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.getCalendarManager().startup({
onResult: function() {
gServer = new CalDavServer("xpcshell@example.com");
gServer.start();
cal.getTimezoneService().startup({
onResult: function() {
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 GenericRequest(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 GenericRequest(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 GenericRequest(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 CalDavRequest/CalDavResponse
*/
add_task(async function test_generic_request() {
gServer.reset();
let uri = gServer.uri("/requests/generic");
let headers = { "X-Hdr": "exists" };
let request = new GenericRequest(
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 GenericRequest(
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 ItemRequest(
gServer.session,
gMockCalendar,
uri,
cal.createEvent(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 ItemRequest(
gServer.session,
gMockCalendar,
uri,
cal.createEvent(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 ItemRequest(gServer.session, gMockCalendar, uri, cal.createEvent(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 DeleteItemRequest(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 DeleteItemRequest(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 PropfindRequest(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 DAVHeaderRequest(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 PrincipalPropertySearchRequest(
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 OutboxRequest(
gServer.session,
gMockCalendar,
uri,
"xpcshell@example.com",
["recipient1@example.com", "recipient2@example.com"],
"REPLY",
cal.createEvent(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 FreeBusyRequest(
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;
strictEqual(first, response.data["mailto:recipient1@example.com"]);
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", "20180103:20180117", "20180118:20180125", "20180126:20180201"]
);
});
add_task(async function test_caldav_client() {
let client = await gServer.getClient();
let pclient = cal.async.promisifyCalendar(client);
let items = await pclient.getAllItems();
equal(items.length, 1);
});

Просмотреть файл

@ -19,6 +19,7 @@
[test_bug668222.js]
[test_bug759324.js]
[test_calmgr.js]
[test_caldav_requests.js]
[test_itip_utils.js]
[test_data_bags.js]
[test_datetime.js]