Revision control

Copy as Markdown

Other Tools

/* 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/. */
/**
* Tests that playing back an offline modification does not corrupt other
* events in the cache. The cached calendar hands its cache-update callback to
* the CalDAV calendar through a member field; when that field outlives its
* operation, the next direct modification - the offline playback - runs the
* leftover callback and writes its own item paired with the previous
* operation's old item, deleting that event from the cache.
*/
var { CalDAVServer } = ChromeUtils.importESModule(
);
var { TestUtils } = ChromeUtils.importESModule("resource://testing-common/TestUtils.sys.mjs");
// The playback chain (refresh, playback, cache update) can take a moment, so
// poll longer than the TestUtils default.
const WAIT_INTERVAL = 200;
const WAIT_TRIES = 100;
const UID_A = "event-a";
const UID_B = "event-b";
const PATH_A = "/calendars/alice/test/event-a.ics";
const PATH_B = "/calendars/alice/test/event-b.ics";
function buildIcs(uid, summary) {
return [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Test//Test//EN",
"BEGIN:VEVENT",
"UID:" + uid,
"SUMMARY:" + summary,
"DTSTART:20220317T120000Z",
"DTEND:20220317T130000Z",
"DTSTAMP:20220317T100000Z",
"END:VEVENT",
"END:VCALENDAR",
].join("\r\n");
}
function registerTestCalendar() {
const calendar = cal.manager.createCalendar(
"caldav",
Services.io.newURI(`${CalDAVServer.origin}/calendars/alice/test/`)
);
calendar.name = "offlinePlayback";
calendar.id = cal.getUUID();
calendar.setProperty("cache.enabled", true);
calendar.setProperty("username", "alice");
cal.manager.registerCalendar(calendar);
const registered = cal.manager.getCalendarById(calendar.id);
return new Promise(resolve => {
const observer = {
QueryInterface: ChromeUtils.generateQI(["calIObserver"]),
onStartBatch() {},
onEndBatch() {},
onAddItem() {},
onModifyItem() {},
onDeleteItem() {},
onError() {},
onPropertyChanged() {},
onPropertyDeleting() {},
onLoad() {
registered.removeObserver(observer);
registered.setProperty("refreshInterval", "0");
resolve(registered);
},
};
registered.addObserver(observer);
});
}
/**
* Waits until the playback of the offline modification has completed: the
* server holds the modification and the item's offline flag has been reset,
* which happens after the cache-update callback - the one that must not leak -
* has already run.
*
* @param {calICalendar} calendar - The registered cached calendar.
* @param {string} path - The item's path on the CalDAVServer.
* @param {string} uid - The item's UID.
* @param {string} needle - Text the item on the server must contain.
*/
async function waitForPlayback(calendar, path, uid, needle) {
await TestUtils.waitForCondition(
() => CalDAVServer.items.get(path)?.ics.includes(needle),
"waiting for the offline modification to reach the server",
WAIT_INTERVAL,
WAIT_TRIES
);
const storage = calendar.wrappedJSObject.mCachedCalendar;
await TestUtils.waitForCondition(
async () => {
const item = await storage.getItem(uid);
return item && !(await storage.getItemOfflineFlag(item));
},
"waiting for the offline flag to be reset after the playback",
WAIT_INTERVAL,
WAIT_TRIES
);
}
add_setup(async function () {
do_get_profile();
CalDAVServer.open("alice", "alice");
const loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
loginInfo.init(CalDAVServer.origin, null, "test", "alice", "alice", "", "");
await Services.logins.addLoginAsync(loginInfo);
registerCleanupFunction(() => {
CalDAVServer.close();
});
});
add_task(async function testOfflineModifyPlaybackKeepsOtherEvents() {
// The server starts out with events A and B.
await CalDAVServer.putItemInternal(PATH_A, buildIcs(UID_A, "A"));
await CalDAVServer.putItemInternal(PATH_B, buildIcs(UID_B, "B"));
// Subscribing to the calendar syncs both events into the cache.
const calendar = await registerTestCalendar();
const a = await calendar.getItem(UID_A);
Assert.ok(a && (await calendar.getItem(UID_B)), "both events were synced");
// Modify A while online. This is the operation whose cache callback must not
// survive into the playback further down.
const aChanged = a.clone();
aChanged.title = "A modified";
await calendar.modifyItem(aChanged, a);
// Modify B while offline. The change only reaches the cache and is flagged
// there for the next synchronization.
const wasManaged = Services.io.manageOfflineStatus;
Services.io.manageOfflineStatus = false;
Services.io.offline = true;
try {
const bCached = await calendar.getItem(UID_B);
const bChanged = bCached.clone();
bChanged.title = "B modified";
await calendar.modifyItem(bChanged, bCached);
} finally {
Services.io.offline = false;
Services.io.manageOfflineStatus = wasManaged;
}
// Back online, the refresh plays B's offline modification back to the server.
await calendar.refresh();
await waitForPlayback(calendar, PATH_B, UID_B, "B modified");
// A must be untouched by that playback - it is the event a leaked callback
// would delete - and each event must exist exactly once.
const aAfter = await calendar.getItem(UID_A);
Assert.ok(aAfter, "event A still exists after the playback of B");
Assert.equal(aAfter?.title, "A modified", "event A kept its modification");
const all = await calendar.getItemsAsArray(Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null);
Assert.equal(all.filter(item => item.id == UID_A).length, 1, "exactly one copy of A");
Assert.equal(all.filter(item => item.id == UID_B).length, 1, "exactly one copy of B");
cal.manager.unregisterCalendar(calendar);
});