VulpesOS/gaia/apps/calendar/test/unit/provider/caldav_test.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

1071 lines
26 KiB
JavaScript
Executable file

define(function(require) {
'use strict';
var AbstractProvider = require('provider/abstract');
var CaldavProvider = require('provider/caldav');
var CaldavPullEvents = require('provider/caldav_pull_events');
var CalendarError = require('common/error');
var Calc = require('common/calc');
var Factory = require('test/support/factory');
var ICAL = require('ext/ical');
var Responder = require('common/responder');
var ServiceSupport = require('test/service/helper');
var core = require('core');
var nextTick = require('common/next_tick');
suite('provider/caldav', function() {
var subject;
var controller;
var db;
var accountStore;
var calendarStore;
var componentStore;
var eventStore;
setup(function(done) {
controller = core.serviceController;
db = core.db;
subject = new CaldavProvider();
var storeFactory = core.storeFactory;
calendarStore = storeFactory.get('Calendar');
accountStore = storeFactory.get('Account');
componentStore = storeFactory.get('IcalComponent');
eventStore = storeFactory.get('Event');
db.open(done);
});
testSupport.calendar.accountEnvironment(
null,
{
_id: 'one',
accountId: 'one',
firstEventSyncDate: null,
lastEventSyncToken: null
}
);
var account;
var calendar;
setup(function() {
account = this.account;
calendar = this.calendar;
});
var ical;
suiteSetup(function(done) {
ical = new ServiceSupport.Fixtures('ical');
ical.load('daily_event');
ical.load('recurring_event');
ical.onready = done;
});
teardown(function(done) {
testSupport.calendar.clearStore(
db,
done
);
});
teardown(function() {
db.close();
});
test('initialization', function() {
assert.instanceOf(
subject,
AbstractProvider
);
});
test('capabilites', function() {
assert.isTrue(subject.useUrl);
assert.isTrue(subject.useCredentials);
});
suite('#calendarCapabilities', function() {
/**
* Creates a test for a privilege set
*/
function testPrivilege(name, privileges, expected) {
test(name, function() {
var calendar = Factory('calendar', {
remote: {
privilegeSet: privileges
}
});
var result = subject.calendarCapabilities(
calendar
);
assert.hasProperties(result, expected);
});
}
testPrivilege('missing privilegeSet', null, {
canUpdateEvent: true,
canDeleteEvent: true,
canCreateEvent: true
});
testPrivilege('delete', ['unbind'], {
canDeleteEvent: true
});
testPrivilege('create/update', ['write-content'], {
canCreateEvent: true,
canUpdateEvent: true
});
});
suite('#_handleServiceError', function() {
var permErrors = [
{ input: 'caldav-authentication', expect: 'Authentication' },
{ input: 'caldav-invalid-entrypoint', expect: 'InvalidServer' }
];
function validatePermError(error) {
suite(error.input, function() {
var result;
var givenErr;
var expectedClass;
setup(function(done) {
expectedClass = CalendarError[error.expect];
givenErr = { name: error.input };
result = subject._handleServiceError(
givenErr,
{ account: account }
);
accountStore.once('update', function(id, model) {
if (id === account._id) {
done();
}
});
});
test('returned value', function() {
assert.instanceOf(result, expectedClass);
});
test('updated error', function() {
assert.ok(account.error, 'has error');
assert.equal(
account.error.name,
(new expectedClass()).name,
'marks error with name'
);
});
});
}
permErrors.forEach(validatePermError);
test('generic error', function() {
var result = subject._handleServiceError(
{ name: 'caldav-server-failure' },
{ account: account }
);
assert.instanceOf(result, CalendarError);
});
test('new account', function() {
var account = Factory('account');
var result = subject._handleServiceError(
{ name: 'caldav-authentication' },
{ account: account }
);
assert.instanceOf(result, CalendarError);
});
});
suite('#eventCapabilities', function() {
test('recurring', function(done) {
var event = Factory('event', {
calendarId: this.calendar._id,
remote: {
isRecurring: true
}
});
var expected = {
canUpdate: false,
canDelete: false,
canCreate: false
};
subject.eventCapabilities(event, function(err, actual) {
done(function() {
assert.deepEqual(actual, expected);
});
});
});
test('without calendar permissions', function(done) {
var event = Factory('event', {
calendarId: this.calendar._id
});
assert.isFalse(event.remote.isRecurring);
var expected = {
canUpdate: true,
canCreate: true,
canDelete: true
};
subject.eventCapabilities(event, function(err, actual) {
done(function() {
assert.deepEqual(actual, expected);
});
});
});
test('with calendar permissions', function(done) {
subject.calendarCapabilities = function() {
return {
canCreateEvent: false,
canDeleteEvent: true,
canUpdateEvent: true
};
};
var event = Factory('event', {
calendarId: calendar._id
});
subject.eventCapabilities(event, function(err, caps) {
done(function() {
assert.deepEqual(caps, {
canCreate: false,
canUpdate: true,
canDelete: true
});
});
});
});
});
suite('methods that wrap request', function() {
var calledWith;
var error;
var result;
var input;
var realMark;
setup(function() {
input = Factory('account');
// mock out real markWith Error
realMark = accountStore.markWithError;
accountStore.markWithError = function() {};
});
teardown(function() {
accountStore.markWithError = realMark;
});
/**
* Validate error handling for a given request.
*
*
* request(validateErrorHandling(done, ['calendar', 'account']));
*
*/
function validateErrorHandling(done, fields) {
error = { name: 'caldav-authentication' };
var pending = 2;
function next() {
--pending;
if (pending === 0) {
nextTick(done);
}
}
var realHandleServiceError = subject._handleServiceError;
subject._handleServiceError = function(givenErr, detail) {
assert.equal(givenErr, error, 'sends error');
fields.forEach(function(field) {
assert.ok(detail[field], 'detail.' + field);
});
next();
return realHandleServiceError.apply(this, arguments);
};
return function(cbError) {
assert.ok(cbError, 'callback recieves error');
next();
};
}
setup(function() {
controller.request = function() {
calledWith = Array.prototype.slice.call(arguments);
var cb = arguments[arguments.length - 1];
setTimeout(function() {
cb(error, result);
}, 0);
};
});
suite('#getAccount', function() {
test('success', function(done) {
result = { id: 'wow' };
subject.getAccount(input, function cb(cbError, cbResult) {
done(function() {
assert.equal(cbResult, result);
assert.equal(cbError, error);
});
});
});
test('error handling', function(done) {
subject.getAccount(
input,
validateErrorHandling(done, ['account'])
);
});
test('offline handling', function(done) {
subject.isOffline = function() { return true; };
subject.getAccount(input, function cb(cbError, cbResult) {
done(function() {
delete subject.isOffline;
assert.equal(cbError.name, 'offline');
});
});
});
});
suite('#findCalendars', function() {
test('error', function(done) {
subject.findCalendars(
input,
validateErrorHandling(done, ['account'])
);
});
test('success', function(done) {
result = {
one: Factory.build('caldav.calendar'),
two: Factory.build('caldav.calendar', { color: null })
};
error = null;
subject.findCalendars(input, function cb(cbError, cbResult) {
done(function() {
assert.equal(
cbResult.one,
result.one,
'does not process events with color'
);
// hack clone
var withColor = JSON.parse(JSON.stringify(result.two));
withColor.color = subject.defaultColor;
assert.deepEqual(
cbResult.two,
withColor,
'adds color'
);
assert.equal(cbError, error);
});
});
});
test('offline handling', function(done) {
subject.isOffline = function() { return true; };
subject.findCalendars(input, function cb(cbError, cbResult) {
done(function() {
delete subject.isOffline;
assert.equal(cbError.name, 'offline');
});
});
});
});
suite('#createEvent', function() {
var event;
var id;
setup(function() {
error = null;
event = Factory('event', {
calendarId: calendar._id
});
result = Factory.create('event').remote;
result.id = 'foo';
result.syncToken = 'hit';
result.icalComponent = 'xfoo';
id = calendar._id + '-foo';
});
suite('success', function() {
setup(function(done) {
subject.createEvent(event, done);
});
test('icalComponent', function(done) {
componentStore.get(id, function(err, result) {
done(function() {
assert.deepEqual(result, {
eventId: id,
ical: 'xfoo'
});
});
});
});
test('event', function(done) {
eventStore.get(id, function(err, result) {
var remote = result.remote;
assert.equal(remote.id, 'foo');
assert.equal(remote.syncToken, 'hit');
assert.ok(!remote.icalComponent, 'does not have icalComponent');
done();
});
});
});
test('error handling', function(done) {
subject.createEvent(
event,
validateErrorHandling(done, ['account', 'calendar'])
);
});
test('offline handling', function(done) {
subject.isOffline = function() { return true; };
subject.createEvent(event, function cb(cbError, cbResult) {
done(function() {
delete subject.isOffline;
assert.equal(cbError.name, 'offline');
});
});
});
});
suite('#updateEvent', function() {
var event;
var component;
setup(function(done) {
var trans = db.transaction(
['events', 'icalComponents'],
'readwrite'
);
trans.oncomplete = function() {
done();
};
event = Factory('event', {
calendarId: calendar._id
});
component = Factory('icalComponent', {
eventId: event._id,
ical: 'original'
});
eventStore.persist(event, done);
componentStore.persist(component, done);
});
suite('success', function() {
setup(function(done) {
error = null;
result = Factory.create('event').remote;
result.icalComponent = 'xfooo';
result.syncToken = 'changedmefoo';
subject.updateEvent(event, done);
});
test('sent data', function() {
var details = calledWith[calledWith.length - 2];
assert.ok(details.event, 'sends event');
assert.equal(
details.icalComponent,
'original',
'icalComponent'
);
});
test('component', function(done) {
componentStore.get(event._id, function(err, item) {
done(function() {
assert.deepEqual(
item,
{ eventId: event._id, ical: 'xfooo' }
);
});
});
});
test('event', function(done) {
eventStore.get(event._id, function(err, item) {
done(function() {
assert.equal(item.remote.syncToken, result.syncToken);
});
});
});
});
test('error handling', function(done) {
subject.updateEvent(
event,
validateErrorHandling(done, ['calendar', 'account'])
);
});
test('offline handling', function(done) {
subject.isOffline = function() { return true; };
subject.updateEvent(event, function cb(cbError, cbResult) {
done(function() {
delete subject.isOffline;
assert.equal(cbError.name, 'offline');
});
});
});
});
suite('#deleteEvent', function() {
var event;
setup(function() {
event = Factory('event', {
calendarId: calendar._id
});
});
test('failure', function(done) {
subject.deleteEvent(
event,
validateErrorHandling(done, ['account', 'calendar'])
);
});
test('success', function(done) {
subject.deleteEvent(event, function() {
done(function() {
assert.equal(calledWith[0], 'caldav');
assert.equal(calledWith[1], 'deleteEvent');
var slice = calledWith.slice(2, 5);
assert.hasProperties(
slice[0], account,
'sends account'
);
assert.hasProperties(
slice[1],
calendar.remote,
'sends calendar'
);
assert.deepEqual(
slice[2],
event.remote,
'sends event'
);
});
});
});
test('offline handling', function(done) {
var event = Factory('event', {
calendarId: calendar._id
});
subject.isOffline = function() { return true; };
subject.deleteEvent(event, function cb(cbError, cbResult) {
done(function() {
delete subject.isOffline;
assert.equal(cbError.name, 'offline');
});
});
});
});
});
suite('#_cachedEventsFor', function() {
var events = [];
var calendar;
setup(function(done) {
events.length = 0;
calendar = Factory('calendar');
calendarStore.persist(calendar, done);
});
// create some events
var i = 0;
/*jshint loopfunc: true */
for (; i < 2; i++) {
setup(function(done) {
var event = Factory('event', {
calendarId: calendar._id,
remote: {
id: i,
url: 'some_foo_' + i + '.ics',
syncToken: i
}
});
events.push(event);
eventStore.persist(event, done);
});
}
test('result', function(done) {
var expected = Object.create(null);
events.forEach(function(item) {
expected[item.remote.url] = {
syncToken: item.remote.syncToken,
id: item._id
};
});
subject._cachedEventsFor(calendar, function(err, result) {
done(function() {
assert.deepEqual(result, expected);
});
});
});
});
suite('#syncEvents', function() {
var events = [];
var calledWith;
function addEvent(cb) {
setup(function(done) {
var event = cb();
events.push(event);
eventStore.persist(event, done);
});
}
setup(function(done) {
calledWith = null;
events.length = 0;
subject._syncEvents = function() {
calledWith = arguments;
var cb = calledWith[calledWith.length - 1];
setTimeout(cb, 0, null);
};
var trans = db.transaction(
['accounts', 'calendars'],
'readwrite'
);
trans.oncomplete = function() {
done();
};
});
suite('sync with cached events', function() {
addEvent(function() {
return Factory('event', {
calendarId: calendar._id,
remote: {
url: 'one.ics',
syncToken: 'one'
}
});
});
test('result', function(done) {
subject.syncEvents(account, calendar, function() {
done(function() {
assert.equal(calledWith[0], account, 'has account');
assert.equal(calledWith[1], calendar, 'has calendar');
// expected cached events (url -> sync token)
var sentCache = calledWith[2];
assert.ok(sentCache, 'sends cache');
assert.ok(sentCache[events[0].remote.url], 'sends url');
});
});
});
test('offline handling', function(done) {
subject.isOffline = function() { return true; };
subject.syncEvents(account, calendar, function cb(cbError, cbResult) {
done(function() {
delete subject.isOffline;
assert.equal(cbError.name, 'offline');
});
});
});
});
suite('sync tokens match', function() {
setup(function() {
calendar.lastEventSyncToken = 'sync';
calendar.remote.syncToken = 'sync';
});
test('result', function(done) {
var isAsync = false;
// tokens match should not sync!
subject.syncEvents(account, calendar, function() {
assert.ok(!calledWith);
assert.ok(isAsync, 'should be async');
done();
});
isAsync = true;
});
test('offline handling', function(done) {
subject.isOffline = function() { return true; };
subject.syncEvents(account, calendar, function cb(cbError, cbResult) {
done(function() {
delete subject.isOffline;
assert.equal(cbError.name, 'offline');
});
});
});
});
});
suite('#_syncEvents', function() {
var calledWith;
var cached = {};
var error;
setup(function(done) {
// add an error to calendar to simulate clearing the error
calendar.error = {};
calendarStore.persist(calendar, done);
});
setup(function() {
error = null;
subject.service.stream = function() {
var args = Array.slice(arguments);
if (args.shift() !== 'caldav') {
throw new Error('expected caldav service');
}
if (args.shift() !== 'streamEvents') {
throw new Error('expected streamEvents');
}
calledWith = args;
var stream = new Responder();
stream.request = function(callback) {
setTimeout(callback, 0, error);
};
return stream;
};
});
test('with error', function(done) {
error = { name: 'caldav-authentication' };
function oncomplete(cbErr) {
done(function() {
assert.instanceOf(cbErr, CalendarError.Authentication);
assert.deepEqual(cbErr.detail, {
account: account,
calendar: calendar
});
});
}
subject._syncEvents(
account,
calendar,
cached,
oncomplete
);
});
suite('with first syncDate', function() {
var expectedSyncDate = new Date(2012, 0, 1);
setup(function(done) {
calendar.firstEventSyncDate = expectedSyncDate;
calendarStore.persist(calendar, done);
});
test('result', function(done) {
var expectedDate = new Date(expectedSyncDate.valueOf());
expectedDate.setDate(
expectedDate.getDate() - subject.daysToSyncInPast
);
var options = {
startDate: expectedDate,
cached: cached
};
// simulate error clearing
calendar.error = {};
var pull = subject._syncEvents(
account,
calendar,
cached,
oncomplete
);
function oncomplete() {
var expected = [
account.toJSON(),
calendar.remote,
options
];
assert.deepEqual(
calledWith,
expected
);
assert.instanceOf(
pull,
CaldavPullEvents
);
assert.equal(pull.account, account);
assert.equal(pull.calendar, calendar);
calendarStore.get(calendar._id, function(err, result) {
done(function() {
assert.ok(!result.error, 'clears errors');
assert.equal(
result.lastEventSyncToken,
result.remote.syncToken,
'updates sync token'
);
assert.deepEqual(
result.firstEventSyncDate,
expectedSyncDate,
'first sync date is never modified'
);
});
});
}
});
});
test('without first sync date', function(done) {
var syncDate = Calc.createDay(new Date());
var expectedSyncDate = new Date(syncDate.valueOf());
syncDate.setDate(syncDate.getDate() - subject.daysToSyncInPast);
calendar.firstEventSyncDate = null;
var options = {
startDate: syncDate,
cached: cached
};
subject._syncEvents(account, calendar, cached, oncomplete);
function oncomplete() {
var expected = [
account.toJSON(),
calendar.remote,
options
];
assert.deepEqual(
calledWith,
expected
);
calendarStore.get(calendar._id, function(err, record) {
done(function() {
assert.deepEqual(
record.firstEventSyncDate,
expectedSyncDate,
'updates first sync date'
);
assert.equal(
record.lastEventSyncToken,
record.remote.syncToken,
'updates sync token'
);
});
});
}
});
});
suite('#ensureRecurrencesExpanded', function() {
var maxDate = new Date(2013, 2, 17);
test('with no icalComponent records', function(done) {
subject.ensureRecurrencesExpanded(maxDate, function(err, didExpand) {
done(function() {
assert.isFalse(didExpand);
});
});
});
test('with lastRecurrenceId after maxDate', function(done) {
var comp = Factory('icalComponent', {
ical: '',
eventId: 22,
lastRecurrenceId: new Date(2013, 2, 18)
});
componentStore.persist(comp, function(err) {
if (err) {
return done(err);
}
subject.ensureRecurrencesExpanded(maxDate, function(err, didExpand) {
if (err) {
return done(err);
}
done(function() {
assert.isFalse(didExpand, 'expanded');
});
});
});
});
suite('with simulated pre-expansion component', function() {
/*jshint -W027 */
// this test still is a little too much for travis
// CI... we need to ensure it is more reliable soon.
return;
var comp;
var didExpand;
var eventId;
var givenLastRecur = new Date(2012, 10, 1);
setup(function() {
var jCal = ICAL.parse(ical.recurringEvent);
var vcal = new ICAL.Component(jCal[1]);
var vevent = vcal.getFirstSubcomponent('vevent');
eventId = calendar._id + '-' + vevent.getFirstPropertyValue('uid');
});
setup(function(done) {
didExpand = false;
comp = Factory('icalComponent', {
calendarId: calendar._id,
eventId: eventId,
ical: ical.recurringEvent,
iterator: {},
lastRecurrenceId: Calc.dateToTransport(
givenLastRecur
)
});
componentStore.persist(comp, done);
core.serviceController.start();
});
setup(function(done) {
subject.ensureRecurrencesExpanded(maxDate, function(err, result) {
didExpand = result;
done();
});
});
test('after expansion', function(done) {
assert.isTrue(didExpand, 'has expanded');
var stores = ['icalComponents', 'busytimes'];
var trans = db.transaction(stores);
var pending = 0;
var results = {};
function next() {
if (!(--pending)) {
done(complete);
}
}
stores.forEach(function(store) {
pending++;
var obj = trans.objectStore(store);
obj.mozGetAll().onsuccess = function(e) {
results[store] = e.target.result;
next();
};
});
function complete() {
assert.length(results.icalComponents, 1);
assert.ok(results.busytimes.length, 1);
var comp = results.icalComponents[0];
var lastRecur = Calc.dateFromTransport(
comp.lastRecurrenceId
);
var dates = [];
results.busytimes.forEach(function(time) {
dates.push(time.startDate);
});
dates = dates.sort();
assert.isTrue(
dates[0].valueOf() > givenLastRecur.valueOf(),
'first occurrence is after last given'
);
assert.isTrue(
dates[dates.length - 1].valueOf() < maxDate.valueOf(),
'last occurrence is before max date'
);
assert.isTrue(
lastRecur.valueOf() > givenLastRecur.valueOf(),
'should expand beyond the given lastRecurId'
);
}
});
});
});
});
});