From 43462d3fc447ac8d843e98e9dc596e21302b1353 Mon Sep 17 00:00:00 2001 From: Hendrik Leppelsack Date: Thu, 18 Feb 2016 16:33:26 +0100 Subject: integrate dav library instead of loading via bower --- js/dav/test/unit/camelize_test.js | 23 ++ js/dav/test/unit/client_test.js | 265 +++++++++++++++++++++ js/dav/test/unit/data/address_book_query.xml | 72 ++++++ js/dav/test/unit/data/calendar_query.xml | 24 ++ js/dav/test/unit/data/current_user_principal.xml | 13 + js/dav/test/unit/data/index.js | 23 ++ js/dav/test/unit/data/propfind.xml | 39 +++ js/dav/test/unit/data/sync_collection.xml | 16 ++ js/dav/test/unit/nock_wrapper.js | 47 ++++ js/dav/test/unit/parser_test.js | 66 +++++ .../test/unit/request/address_book_query_test.js | 69 ++++++ js/dav/test/unit/request/basic_test.js | 63 +++++ js/dav/test/unit/request/calendar_query_test.js | 107 +++++++++ js/dav/test/unit/request/propfind_test.js | 84 +++++++ js/dav/test/unit/request/sync_collection_test.js | 65 +++++ js/dav/test/unit/sandbox_test.js | 72 ++++++ js/dav/test/unit/template_test.js | 69 ++++++ js/dav/test/unit/transport_test.js | 256 ++++++++++++++++++++ js/dav/test/unit/xmlhttprequest_test.js | 81 +++++++ 19 files changed, 1454 insertions(+) create mode 100644 js/dav/test/unit/camelize_test.js create mode 100644 js/dav/test/unit/client_test.js create mode 100644 js/dav/test/unit/data/address_book_query.xml create mode 100644 js/dav/test/unit/data/calendar_query.xml create mode 100644 js/dav/test/unit/data/current_user_principal.xml create mode 100644 js/dav/test/unit/data/index.js create mode 100644 js/dav/test/unit/data/propfind.xml create mode 100644 js/dav/test/unit/data/sync_collection.xml create mode 100644 js/dav/test/unit/nock_wrapper.js create mode 100644 js/dav/test/unit/parser_test.js create mode 100644 js/dav/test/unit/request/address_book_query_test.js create mode 100644 js/dav/test/unit/request/basic_test.js create mode 100644 js/dav/test/unit/request/calendar_query_test.js create mode 100644 js/dav/test/unit/request/propfind_test.js create mode 100644 js/dav/test/unit/request/sync_collection_test.js create mode 100644 js/dav/test/unit/sandbox_test.js create mode 100644 js/dav/test/unit/template_test.js create mode 100644 js/dav/test/unit/transport_test.js create mode 100644 js/dav/test/unit/xmlhttprequest_test.js (limited to 'js/dav/test/unit') diff --git a/js/dav/test/unit/camelize_test.js b/js/dav/test/unit/camelize_test.js new file mode 100644 index 00000000..f956d5e0 --- /dev/null +++ b/js/dav/test/unit/camelize_test.js @@ -0,0 +1,23 @@ +import { assert } from 'chai'; + +import camelize from '../../lib/camelize'; + +suite('camelize', function() { + test('single word', function() { + assert.strictEqual(camelize('green'), 'green'); + }); + + test('multiple words', function() { + assert.strictEqual( + camelize('green-eggs-and-ham', '-'), + 'greenEggsAndHam' + ); + }); + + test('omit delimiter', function() { + assert.strictEqual( + camelize('green_eggs_and_ham'), + 'greenEggsAndHam' + ); + }); +}); diff --git a/js/dav/test/unit/client_test.js b/js/dav/test/unit/client_test.js new file mode 100644 index 00000000..dac7f5c9 --- /dev/null +++ b/js/dav/test/unit/client_test.js @@ -0,0 +1,265 @@ +import sinon from 'sinon'; + +import * as dav from '../../lib'; + +suite('Client', function() { + let client, xhr, send; + + setup(function() { + xhr = new dav.transport.Basic( + new dav.Credentials({ + username: 'Killer BOB', + password: 'blacklodge' + }) + ); + + send = sinon.stub(xhr, 'send'); + client = new dav.Client(xhr, { baseUrl: 'https://mail.mozilla.com' }); + }); + + teardown(function() { + send.restore(); + }); + + test('#send', function() { + let url = 'https://mail.mozilla.com/'; + let req = dav.request.basic({ + method: 'PUT', + data: 'BEGIN:VCALENDAR\nEND:VCALENDAR', + etag: 'abc123' + }); + + let sandbox = dav.createSandbox(); + client.send(req, url, { sandbox: sandbox }); + sinon.assert.calledWith(send, req, url, { sandbox: sandbox }); + }); + + test('#send with relative url', function() { + let req = dav.request.basic({ + method: 'PUT', + data: 'BEGIN:VCALENDAR\nEND:VCALENDAR', + etag: 'abc123' + }); + + client.send(req, '/calendars/123.ics'); + sinon.assert.calledWith( + send, + req, + 'https://mail.mozilla.com/calendars/123.ics' + ); + }); + + suite('accounts', function() { + let createAccount; + + setup(function() { + createAccount = sinon.stub(client._accounts, 'createAccount'); + }); + + teardown(function() { + createAccount.restore(); + }); + + test('createAccount', function() { + client.createAccount({ sandbox: {}, server: 'http://dav.example.com' }); + sinon.assert.calledWith(createAccount, { + sandbox: {}, + server: 'http://dav.example.com', + xhr: xhr + }); + }); + }); + + suite('calendars', function() { + let createCalendarObject, + updateCalendarObject, + deleteCalendarObject, + syncCalendar, + syncCaldavAccount; + + setup(function() { + createCalendarObject = sinon.stub( + client._calendars, + 'createCalendarObject' + ); + updateCalendarObject = sinon.stub( + client._calendars, + 'updateCalendarObject' + ); + deleteCalendarObject = sinon.stub( + client._calendars, + 'deleteCalendarObject' + ); + syncCalendar = sinon.stub(client._calendars, 'syncCalendar'); + syncCaldavAccount = sinon.stub(client._calendars, 'syncCaldavAccount'); + }); + + teardown(function() { + createCalendarObject.restore(); + updateCalendarObject.restore(); + deleteCalendarObject.restore(); + syncCalendar.restore(); + syncCaldavAccount.restore(); + }); + + test('#createCalendarObject', function() { + let calendar = new dav.Calendar(); + client.createCalendarObject(calendar, { + data: 'BEGIN:VCALENDAR\nEND:VCALENDAR', + filename: 'test.ics' + }); + sinon.assert.calledWith( + createCalendarObject, + calendar, + { + data: 'BEGIN:VCALENDAR\nEND:VCALENDAR', + filename: 'test.ics', + xhr: xhr + } + ); + }); + + test('#updateCalendarObject', function() { + let object = new dav.CalendarObject(); + client.updateCalendarObject(object); + sinon.assert.calledWith( + updateCalendarObject, + object, + { + xhr: xhr + } + ); + }); + + test('#deleteCalendarObject', function() { + let object = new dav.CalendarObject(); + client.deleteCalendarObject(object); + sinon.assert.calledWith( + deleteCalendarObject, + object, + { + xhr: xhr + } + ); + }); + + test('#syncCalendar', function() { + let calendar = new dav.Calendar(); + client.syncCalendar(calendar, { syncMethod: 'webdav' }); + sinon.assert.calledWith( + syncCalendar, + calendar, + { + syncMethod: 'webdav', + xhr: xhr + } + ); + }); + + test('#syncCaldavAccount', function() { + let account = new dav.Account(); + client.syncCaldavAccount(account, { syncMethod: 'webdav' }); + sinon.assert.calledWith( + syncCaldavAccount, + account, + { + syncMethod: 'webdav', + xhr: xhr + } + ); + }); + }); + + suite('contacts', function() { + let createCard, + updateCard, + deleteCard, + syncAddressBook, + syncCarddavAccount; + + setup(function() { + createCard = sinon.stub(client._contacts, 'createCard'); + updateCard = sinon.stub(client._contacts, 'updateCard'); + deleteCard = sinon.stub(client._contacts, 'deleteCard'); + syncAddressBook = sinon.stub(client._contacts, 'syncAddressBook'); + syncCarddavAccount = sinon.stub(client._contacts, 'syncCarddavAccount'); + }); + + teardown(function() { + createCard.restore(); + updateCard.restore(); + deleteCard.restore(); + syncAddressBook.restore(); + syncCarddavAccount.restore(); + }); + + test('#createCard', function() { + let addressBook = new dav.AddressBook(); + client.createCard(addressBook, { + data: 'BEGIN:VCARD\nEND:VCARD', + filename: 'test.vcf' + }); + sinon.assert.calledWith( + createCard, + addressBook, + { + data: 'BEGIN:VCARD\nEND:VCARD', + filename: 'test.vcf', + xhr: xhr + } + ); + }); + + test('#updateCard', function() { + let object = new dav.VCard(); + client.updateCard(object); + sinon.assert.calledWith( + updateCard, + object, + { + xhr: xhr + } + ); + }); + + test('#deleteCard', function() { + let object = new dav.VCard(); + client.deleteCard(object); + sinon.assert.calledWith( + deleteCard, + object, + { + xhr: xhr + } + ); + }); + + test('#syncAddressBook', function() { + let addressBook = new dav.AddressBook(); + client.syncAddressBook(addressBook, { + syncMethod: 'basic' + }); + sinon.assert.calledWith( + syncAddressBook, + addressBook, + { + syncMethod: 'basic', + xhr: xhr + } + ); + }); + + test('#syncCarddavAccount', function() { + let account = new dav.Account(); + client.syncCarddavAccount(account, { syncMethod: 'basic' }); + sinon.assert.calledWith( + syncCarddavAccount, + account, + { + syncMethod: 'basic', + xhr: xhr + } + ); + }); + }); +}); diff --git a/js/dav/test/unit/data/address_book_query.xml b/js/dav/test/unit/data/address_book_query.xml new file mode 100644 index 00000000..3e7690b7 --- /dev/null +++ b/js/dav/test/unit/data/address_book_query.xml @@ -0,0 +1,72 @@ + + + + /addressbooks/admin/ + + + + + + + + + + + + + + + + + + + + + HTTP/1.1 200 OK + + + + /addressbooks/admin/default/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HTTP/1.1 200 OK + + + diff --git a/js/dav/test/unit/data/calendar_query.xml b/js/dav/test/unit/data/calendar_query.xml new file mode 100644 index 00000000..3a276b75 --- /dev/null +++ b/js/dav/test/unit/data/calendar_query.xml @@ -0,0 +1,24 @@ + + + /calendars/johndoe/home/132456762153245.ics + + + "2134-314" + BEGIN:VCALENDAR +END:VCALENDAR + + HTTP/1.1 200 OK + + + + /calendars/johndoe/home/132456-34365.ics + + + "5467-323" + BEGIN:VCALENDAR +END:VCALENDAR + + HTTP/1.1 200 OK + + + diff --git a/js/dav/test/unit/data/current_user_principal.xml b/js/dav/test/unit/data/current_user_principal.xml new file mode 100644 index 00000000..c1b3ead7 --- /dev/null +++ b/js/dav/test/unit/data/current_user_principal.xml @@ -0,0 +1,13 @@ + + + / + + + + /principals/admin%40domain.tld/ + + + HTTP/1.1 200 OK + + + diff --git a/js/dav/test/unit/data/index.js b/js/dav/test/unit/data/index.js new file mode 100644 index 00000000..627a39b6 --- /dev/null +++ b/js/dav/test/unit/data/index.js @@ -0,0 +1,23 @@ +import fs from 'fs'; +import { format } from 'util'; + +import camelize from '../../../lib/camelize'; + +let docs = {}; +export default docs; + +[ + 'address_book_query', + 'current_user_principal', + 'calendar_query', + 'propfind', + 'sync_collection' +].forEach(function(responseType) { + var camelCase = camelize(responseType); + docs[camelCase] = fs + .readFileSync( + format('%s/%s.xml', __dirname, responseType), + 'utf-8' + ) + .replace(/>\s+<'); // Remove whitespace between close and open tag. +}); diff --git a/js/dav/test/unit/data/propfind.xml b/js/dav/test/unit/data/propfind.xml new file mode 100644 index 00000000..274520c7 --- /dev/null +++ b/js/dav/test/unit/data/propfind.xml @@ -0,0 +1,39 @@ + + + + /calendars/admin/ + + + + + + + HTTP/1.1 404 Not Found + + + + /calendars/admin/default/ + + + default calendar + http://sabre.io/ns/sync/0 + + + + + + HTTP/1.1 200 OK + + + + /calendars/admin/outbox/ + + + + + + + HTTP/1.1 404 Not Found + + + diff --git a/js/dav/test/unit/data/sync_collection.xml b/js/dav/test/unit/data/sync_collection.xml new file mode 100644 index 00000000..a2f805ce --- /dev/null +++ b/js/dav/test/unit/data/sync_collection.xml @@ -0,0 +1,16 @@ + + + + /calendars/admin/default/test.ics + + + "e91f3c9518f76753a7dc5a0cf8998986" + BEGIN:VCALENDAR +END:VCALENDAR + + + HTTP/1.1 200 OK + + + http://sabre.io/ns/sync/3 + diff --git a/js/dav/test/unit/nock_wrapper.js b/js/dav/test/unit/nock_wrapper.js new file mode 100644 index 00000000..f665a0ea --- /dev/null +++ b/js/dav/test/unit/nock_wrapper.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Decorates nock with some useful utilities. + */ +import { assert } from 'chai'; +import co from 'co'; +import nock from 'nock'; + +export function nockWrapper(url) { + let result = nock(url); + + // This is a hack suggested here https://github.com/pgte/nock#protip + // to intercept the request conditional on the request body. + result.matchRequestBody = (path, method, match, options={}) => { + let statusCode = options.statusCode || 200; + let statusText = options.statusText || '200 OK'; + return result + .filteringRequestBody(body => match(body) ? '*' : '') + .intercept(path, method, '*') + .delay(1) + .reply(statusCode, statusText); + }; + + /** + * Whether or not an error is thrown in the promise, + * the mock should have intercepted the request. + */ + result.verify = co.wrap(function *(promise) { + try { + yield promise; + } catch (error) { + assert.notInclude(error.toString(), 'ECONNREFUSED'); + } finally { + result.done(); + } + }); + + return result; +} + +Object.keys(nock).forEach(key => { + let value = nock[key]; + if (typeof value !== 'function') { + return; + } + + nockWrapper[key] = value.bind(nockWrapper) +}); diff --git a/js/dav/test/unit/parser_test.js b/js/dav/test/unit/parser_test.js new file mode 100644 index 00000000..bd879d71 --- /dev/null +++ b/js/dav/test/unit/parser_test.js @@ -0,0 +1,66 @@ +import { assert } from 'chai'; + +import { multistatus } from '../../lib/parser'; +import data from './data'; + +suite('parser.multistatus', function() { + test('propfind (current-user-principal)', function() { + let currentUserPrincipal = data.currentUserPrincipal; + assert.deepEqual(multistatus(currentUserPrincipal), { + response: [{ + href: '/', + propstat: [{ + prop: { + currentUserPrincipal: '/principals/admin@domain.tld/' + }, + status: 'HTTP/1.1 200 OK' + }] + }] + }); + }); + + test('report (calendar-query)', function() { + let calendarQuery = data.calendarQuery; + assert.deepEqual(multistatus(calendarQuery), { + response: [ + { + href: '/calendars/johndoe/home/132456762153245.ics', + propstat: [{ + prop: { + getetag: '"2134-314"', + calendarData: 'BEGIN:VCALENDAR\nEND:VCALENDAR' + }, + status: 'HTTP/1.1 200 OK' + }] + }, + { + href: '/calendars/johndoe/home/132456-34365.ics', + propstat: [{ + prop: { + getetag: '"5467-323"', + calendarData: 'BEGIN:VCALENDAR\nEND:VCALENDAR' + }, + status: 'HTTP/1.1 200 OK' + }] + }, + ] + }); + }); + + test('report (sync-collection)', function() { + let syncCollection = data.syncCollection; + assert.deepEqual(multistatus(syncCollection), { + response: [{ + href: '/calendars/admin/default/test.ics', + propstat: [{ + prop: { + 'calendarData': 'BEGIN:VCALENDAR\nEND:VCALENDAR\n', + getetag: '"e91f3c9518f76753a7dc5a0cf8998986"' + }, + status: 'HTTP/1.1 200 OK' + }] + }], + syncToken: 'http://sabre.io/ns/sync/3' + }); + }); +}); diff --git a/js/dav/test/unit/request/address_book_query_test.js b/js/dav/test/unit/request/address_book_query_test.js new file mode 100644 index 00000000..31777efd --- /dev/null +++ b/js/dav/test/unit/request/address_book_query_test.js @@ -0,0 +1,69 @@ +import { assert } from 'chai'; +import co from 'co'; + +import * as ns from '../../../lib/namespace'; +import { Request, addressBookQuery } from '../../../lib/request'; +import * as transport from '../../../lib/transport'; +import data from '../data'; +import { nockWrapper } from '../nock_wrapper'; + +suite('request.addressBookQuery', function() { + let xhr; + + setup(function() { + xhr = new transport.Basic({ username: 'admin', password: 'admin' }); + }); + + teardown(() => nockWrapper.cleanAll()); + + test('should set depth header', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchHeader('Depth', 1) + .intercept('/principals/admin/', 'REPORT') + .reply(200); + + let req = addressBookQuery({ + props: [ { name: 'address-data', namespace: ns.CARDDAV } ], + depth: 1 + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/'); + yield mock.verify(send); + })); + + test('should add specified props to report body', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchRequestBody('/principals/admin/', 'REPORT', body => { + return body.indexOf('') !== -1; + }); + + let req = addressBookQuery({ + props: [ { name: 'catdog', namespace: ns.DAV } ] + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/'); + yield mock.verify(send); + })); + + test('should resolve with appropriate data structure', co.wrap(function *() { + nockWrapper('http://127.0.0.1:1337') + .intercept('/', 'REPORT') + .reply(200, data.addressBookQuery); + + + let req = addressBookQuery({ + props: [ + { name: 'getetag', namespace: ns.DAV }, + { name: 'address-data', namespace: ns.CARDDAV } + ] + }); + + let addressBooks = yield xhr.send(req, 'http://127.0.0.1:1337'); + assert.lengthOf(addressBooks, 2); + addressBooks.forEach(addressBook => { + assert.typeOf(addressBook.href, 'string'); + assert.operator(addressBook.href.length, '>', 0); + assert.typeOf(addressBook.props, 'object'); + }); + })); +}); diff --git a/js/dav/test/unit/request/basic_test.js b/js/dav/test/unit/request/basic_test.js new file mode 100644 index 00000000..9ce5f01b --- /dev/null +++ b/js/dav/test/unit/request/basic_test.js @@ -0,0 +1,63 @@ +import { assert } from 'chai'; +import co from 'co'; + +import { Request, basic } from '../../../lib/request'; +import * as transport from '../../../lib/transport'; +import { nockWrapper } from '../nock_wrapper'; + +suite('put', function() { + let xhr; + + setup(function() { + xhr = new transport.Basic({ user: 'admin', password: 'admin' }); + }); + + teardown(() => nockWrapper.cleanAll()); + + test('should set If-Match header', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchHeader('If-Match', '1337') + .intercept('/', 'PUT') + .reply(200); + + let req = basic({ + method: 'PUT', + etag: '1337' + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337'); + yield mock.verify(send); + })); + + test('should send options data as request body', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchRequestBody('/', 'PUT', body => { + return body === 'Bad hair day!'; + }); + + let req = basic({ + method: 'PUT', + data: 'Bad hair day!' + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337'); + yield mock.verify(send); + })); + + test('should throw error on bad response', co.wrap(function *() { + nockWrapper('http://127.0.0.1:1337') + .intercept('/', 'PUT') + .delay(1) + .reply('400', '400 Bad Request'); + + let req = basic({ method: 'PUT' }); + + try { + yield xhr.send(req, 'http://127.0.0.1:1337') + assert.fail('request.basic should have thrown an error'); + } catch (error) { + assert.instanceOf(error, Error); + assert.include(error.toString(), 'Bad status: 400'); + } + })); +}); diff --git a/js/dav/test/unit/request/calendar_query_test.js b/js/dav/test/unit/request/calendar_query_test.js new file mode 100644 index 00000000..a97e4d0b --- /dev/null +++ b/js/dav/test/unit/request/calendar_query_test.js @@ -0,0 +1,107 @@ +import { assert } from 'chai'; +import co from 'co'; + +import * as ns from '../../../lib/namespace'; +import { Request, calendarQuery } from '../../../lib/request'; +import * as transport from '../../../lib/transport'; +import data from '../data'; +import { nockWrapper } from '../nock_wrapper'; + +suite('request.calendarQuery', function() { + let xhr; + + setup(function() { + xhr = new transport.Basic({ username: 'admin', password: 'admin' }); + }); + + teardown(() => nockWrapper.cleanAll()); + + test('should set depth header', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchHeader('Depth', 1) + .intercept('/principals/admin/', 'REPORT') + .reply(200); + + let req = calendarQuery({ + props: [ { name: 'calendar-data', namespace: ns.CALDAV } ], + depth: 1 + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/'); + yield mock.verify(send); + })); + + test('should add specified props to report body', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchRequestBody('/principals/admin/', 'REPORT', body => { + return body.indexOf('') !== -1; + }); + + let req = calendarQuery({ + props: [ { name: 'catdog', namespace: ns.DAV } ] + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/'); + yield mock.verify(send); + })); + + test('should add specified filters to report body', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchRequestBody('/principals/admin/', 'REPORT', body => { + return body.indexOf('') !== -1; + }); + + let req = calendarQuery({ + filters: [{ + type: 'comp-filter', + attrs: { name: 'VCALENDAR' }, + }] + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/'); + yield mock.verify(send); + })); + + test('should add timezone to report body', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchRequestBody('/principals/admin/', 'REPORT', body => { + let data = 'BEGIN:VTIMEZONE\nEND:VTIMEZONE'; + return body.indexOf(data) !== -1; + }); + + let req = calendarQuery({ + url: 'http://127.0.0.1:1337/principals/admin/', + timezone: 'BEGIN:VTIMEZONE\nEND:VTIMEZONE' + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/'); + yield mock.verify(send); + })); + + test('should resolve with appropriate data structure', co.wrap(function *() { + nockWrapper('http://127.0.0.1:1337') + .intercept('/', 'REPORT') + .reply(200, data.calendarQuery); + + let req = calendarQuery({ + props: [ + { name: 'getetag', namespace: ns.DAV }, + { name: 'calendar-data', namespace: ns.CALDAV } + ], + filters: [ { type: 'comp', attrs: { name: 'VCALENDAR' } } ] + }); + + let calendars = yield xhr.send(req, 'http://127.0.0.1:1337/'); + assert.lengthOf(calendars, 2); + calendars.forEach(calendar => { + assert.typeOf(calendar.href, 'string'); + assert.operator(calendar.href.length, '>', 0); + assert.include(calendar.href, '.ics'); + assert.typeOf(calendar.props, 'object'); + assert.typeOf(calendar.props.getetag, 'string'); + assert.operator(calendar.props.getetag.length, '>', 0); + assert.typeOf(calendar.props.calendarData, 'string'); + assert.operator(calendar.props.calendarData.length, '>', 0); + }); + })); +}); diff --git a/js/dav/test/unit/request/propfind_test.js b/js/dav/test/unit/request/propfind_test.js new file mode 100644 index 00000000..378f06fd --- /dev/null +++ b/js/dav/test/unit/request/propfind_test.js @@ -0,0 +1,84 @@ +import { assert } from 'chai'; +import co from 'co'; + +import * as namespace from '../../../lib/namespace'; +import { Request, propfind } from '../../../lib/request'; +import * as transport from '../../../lib/transport'; +import data from '../data'; +import { nockWrapper } from '../nock_wrapper'; + +suite('request.propfind', function() { + let xhr; + + setup(function() { + xhr = new transport.Basic({ user: 'admin', password: 'admin' }); + }); + + teardown(() => nockWrapper.cleanAll()); + + test('should set depth header', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchHeader('Depth', '0') // Will only get intercepted if Depth => 0. + .intercept('/', 'PROPFIND') + .reply(200); + + let req = propfind({ + props: [ { name: 'catdog', namespace: namespace.DAV } ], + depth: '0' + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337'); + yield mock.verify(send); + })); + + test('should add specified properties to propfind body', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchRequestBody('/', 'PROPFIND', function(body) { + return body.indexOf('') !== -1; + }); + + let req = propfind({ + props: [ { name: 'catdog', namespace: namespace.DAV } ], + depth: '0' + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337'); + yield mock.verify(send); + })); + + test('should resolve with appropriate data structure', co.wrap(function *() { + nockWrapper('http://127.0.0.1:1337') + .intercept('/', 'PROPFIND') + .reply(200, data.propfind); + + let req = propfind({ + props: [ + { name: 'displayname', namespace: namespace.DAV }, + { name: 'getctag', namespace: namespace.CALENDAR_SERVER }, + { + name: 'supported-calendar-component-set', + namespace: namespace.CALDAV + } + ], + depth: 1 + }); + + let responses = yield xhr.send(req, 'http://127.0.0.1:1337/'); + + assert.isArray(responses); + responses.forEach(response => { + assert.typeOf(response.href, 'string'); + assert.operator(response.href.length, '>', 0); + assert.ok('props' in response); + assert.typeOf(response.props, 'object'); + if ('displayname' in response.props) { + assert.typeOf(response.props.displayname, 'string'); + assert.operator(response.props.displayname.length, '>', 0); + } + if ('components' in response.props) { + assert.isArray(response.props.components); + assert.include(response.props.components, 'VEVENT'); + } + }); + })); +}); diff --git a/js/dav/test/unit/request/sync_collection_test.js b/js/dav/test/unit/request/sync_collection_test.js new file mode 100644 index 00000000..b595b142 --- /dev/null +++ b/js/dav/test/unit/request/sync_collection_test.js @@ -0,0 +1,65 @@ +import { assert } from 'chai'; +import co from 'co'; + +import * as namespace from '../../../lib/namespace'; +import { Request, syncCollection } from '../../../lib/request'; +import * as transport from '../../../lib/transport'; +import { nockWrapper } from '../nock_wrapper'; + +suite('request.syncCollection', function() { + let xhr; + + setup(function() { + xhr = new transport.Basic({ username: 'admin', password: 'admin' }); + }); + + teardown(() => nockWrapper.cleanAll()); + + test('should add props to request body', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchRequestBody( + '/principals/admin/default/', + 'REPORT', + body => { + return body.indexOf('') !== -1 && + body.indexOf('') !== -1; + } + ); + + let req = syncCollection({ + syncLevel: 1, + syncToken: 'abc123', + props: [ + { name: 'getetag', namespace: namespace.DAV }, + { name: 'calendar-data', namespace: namespace.CALDAV } + ] + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/default/'); + yield mock.verify(send); + })); + + test('should set sync details in request body', co.wrap(function *() { + let mock = nockWrapper('http://127.0.0.1:1337') + .matchRequestBody( + '/principals/admin/default/', + 'REPORT', + body => { + return body.indexOf('1') !== -1 && + body.indexOf('abc123') !== -1; + } + ); + + let req = syncCollection({ + syncLevel: 1, + syncToken: 'abc123', + props: [ + { name: 'getetag', namespace: namespace.DAV }, + { name: 'calendar-data', namespace: namespace.CALDAV } + ] + }); + + let send = xhr.send(req, 'http://127.0.0.1:1337/principals/admin/default/'); + yield mock.verify(send); + })); +}); diff --git a/js/dav/test/unit/sandbox_test.js b/js/dav/test/unit/sandbox_test.js new file mode 100644 index 00000000..bfcd20f3 --- /dev/null +++ b/js/dav/test/unit/sandbox_test.js @@ -0,0 +1,72 @@ +import { assert } from 'chai'; +import sinon from 'sinon'; + +import * as dav from '../../lib'; +import { Sandbox, createSandbox } from '../../lib/sandbox'; +import XMLHttpRequest from '../../lib/xmlhttprequest'; + +suite('sandbox', function() { + let sandbox; + + setup(function() { + sandbox = createSandbox(); + }); + + test('#add', function() { + assert.lengthOf(sandbox.requestList, 0); + let one = new XMLHttpRequest(), + two = new XMLHttpRequest(); + sandbox.add(one); + sandbox.add(two); + assert.lengthOf(sandbox.requestList, 2); + assert.include(sandbox.requestList, one); + assert.include(sandbox.requestList, two); + }); + + test('#abort', function() { + let one = new XMLHttpRequest(), + two = new XMLHttpRequest(); + sandbox.add(one); + sandbox.add(two); + let stubOne = sinon.stub(one, 'abort'), + stubTwo = sinon.stub(two, 'abort'); + sandbox.abort(); + sinon.assert.calledOnce(stubOne); + sinon.assert.calledOnce(stubTwo); + }); +}); + +suite('new sandbox object interface', function() { + let sandbox; + + setup(function() { + sandbox = new Sandbox(); + }); + + test('constructor', function() { + assert.instanceOf(sandbox, Sandbox); + }); + + test('#add', function() { + assert.lengthOf(sandbox.requestList, 0); + let one = new XMLHttpRequest(), + two = new XMLHttpRequest(); + sandbox.add(one); + sandbox.add(two); + assert.lengthOf(sandbox.requestList, 2); + assert.include(sandbox.requestList, one); + assert.include(sandbox.requestList, two); + }); + + test('#abort', function() { + let one = new XMLHttpRequest(), + two = new XMLHttpRequest(); + sandbox.add(one); + sandbox.add(two); + let stubOne = sinon.stub(one, 'abort'), + stubTwo = sinon.stub(two, 'abort'); + sandbox.abort(); + sinon.assert.calledOnce(stubOne); + sinon.assert.calledOnce(stubTwo); + }); +}); diff --git a/js/dav/test/unit/template_test.js b/js/dav/test/unit/template_test.js new file mode 100644 index 00000000..aec09dd2 --- /dev/null +++ b/js/dav/test/unit/template_test.js @@ -0,0 +1,69 @@ +import { assert } from 'chai'; + +import prop from '../../lib/template/prop'; +import filter from '../../lib/template/filter'; + +suite('template helpers', function() { + test('comp-filter', function() { + let item = filter({ + type: 'comp-filter', + attrs: { name: 'VCALENDAR' } + }); + + assert.strictEqual(item, ''); + }); + + test('time-range', function() { + let item = filter({ + type: 'time-range', + attrs: { start: '20060104T000000Z', end: '20060105T000000Z' } + }); + + assert.strictEqual( + item, + '' + ); + }); + + test('time-range no end', function() { + let item = filter({ + type: 'time-range', + attrs: { start: '20060104T000000Z' } + }); + + assert.strictEqual(item, ''); + }); + + test('nested', function() { + let item = filter({ + type: 'comp-filter', + attrs: { name: 'VCALENDAR' }, + children: [{ + type: 'comp-filter', + attrs: { name: 'VEVENT' }, + children: [{ + type: 'time-range', + attrs: { start: '20060104T000000Z', end: '20060105T000000Z' } + }] + }] + }); + + assert.strictEqual( + item.replace(/\s/g, ''), + ('' + + '' + + '' + + '' + + '').replace(/\s/g, '') + ); + }); + + test('prop', function() { + let item = prop({ + name: 'spongebob', + namespace: 'urn:ietf:params:xml:ns:caldav' + }); + + assert.strictEqual(item, ''); + }); +}); diff --git a/js/dav/test/unit/transport_test.js b/js/dav/test/unit/transport_test.js new file mode 100644 index 00000000..24deb01e --- /dev/null +++ b/js/dav/test/unit/transport_test.js @@ -0,0 +1,256 @@ +import { assert } from 'chai'; +import co from 'co'; +import nock from 'nock'; +import sinon from 'sinon'; + +import { Credentials } from '../../lib/model'; +import { createSandbox } from '../../lib/sandbox'; +import { Basic, OAuth2 } from '../../lib/transport'; +import XMLHttpRequest from '../../lib/xmlhttprequest'; + +suite('Basic#send', function() { + let xhr, req; + + setup(function() { + xhr = new Basic( + new Credentials({ username: 'admin', password: 'admin' }) + ); + + req = { + method: 'GET', + transformRequest: xhr => xhr + }; + }); + + teardown(function() { + nock.cleanAll(); + }); + + test('should sandbox xhr', function() { + let sandbox = createSandbox(); + assert.lengthOf(sandbox.requestList, 0); + xhr.send(req, 'http://127.0.0.1:1337', { sandbox: sandbox }); + assert.lengthOf(sandbox.requestList, 1); + }); + + test('should apply `transformRequest`', function() { + let stub = sinon.stub(req, 'transformRequest'); + xhr.send(req, 'http://127.0.0.1:1337'); + sinon.assert.called(stub); + stub.restore(); + }); + + test('should send req', co.wrap(function *() { + let nockObj = nock('http://127.0.0.1:1337') + .get('/') + .reply(200, '200 OK'); + + assert.notOk(nockObj.isDone()); + yield xhr.send(req, 'http://127.0.0.1:1337'); + assert.ok(nockObj.isDone()); + })); + + test('should invoke onerror if error thrown', function(done) { + nock('http://127.0.0.1:1337') + .get('/') + .reply(400, '400 Bad Request'); + + req.onerror = function(error) { + assert.instanceOf(error, Error); + assert.include(error.toString(), 'Bad status: 400'); + done(); + }; + + xhr.send(req, 'http://127.0.0.1:1337'); + }); + + test('should return promise that resolves with xhr', co.wrap(function *() { + nock('http://127.0.0.1:1337') + .get('/') + .reply(200, '200 OK'); + + let value = yield xhr.send(req, 'http://127.0.0.1:1337'); + assert.instanceOf(value, XMLHttpRequest); + assert.strictEqual(value.request.readyState, 4); + })); +}); + +suite('OAuth2#send', function() { + let xhr, req, credentials; + + setup(function() { + credentials = new Credentials({ + clientId: '605300196874-1ki833poa7uqabmh3hq' + + '6u1onlqlsi54h.apps.googleusercontent.com', + clientSecret: 'jQTKlOhF-RclGaGJot3HIcVf', + redirectUrl: 'https://oauth.gaiamobile.org/authenticated', + tokenUrl: 'https://accounts.google.com/o/oauth2/token', + authorizationCode: 'gareth' + }); + + xhr = new OAuth2(credentials); + + req = { method: 'GET' }; + }); + + teardown(function() { + nock.cleanAll(); + }); + + test('should get access token', co.wrap(function *() { + let access = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply( + 200, + JSON.stringify({ + access_token: 'sosafesosecret', + refresh_token: 'lemonade!!1', + expires_in: 12345 + }) + ); + + let mock = nock('http://127.0.0.1:1337') + .get('/') + .matchHeader('Authorization', 'Bearer sosafesosecret') + .reply(200); + + let response = yield xhr.send(req, 'http://127.0.0.1:1337', { + retry: false + }); + + assert.instanceOf(response, XMLHttpRequest); + assert.ok(access.isDone(), 'should get access'); + assert.strictEqual(credentials.accessToken, 'sosafesosecret'); + assert.strictEqual(credentials.refreshToken, 'lemonade!!1'); + assert.operator(credentials.expiration, '>', Date.now()); + assert.ok(mock.isDone(), 'should send req with Authorization header'); + })); + + test('should refresh access token if expired', co.wrap(function *() { + let refresh = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply( + 200, + JSON.stringify({ + access_token: 'Little Bear', + expires_in: 314159 + }) + ); + + let mock = nock('http://127.0.0.1:1337') + .get('/') + .matchHeader('Authorization', 'Bearer Little Bear') + .reply(200, '200 OK'); + + credentials.accessToken = 'EXPIRED'; + credentials.refreshToken = '1/oPHTPFgECWFPrs7KgHdis24u6Xl4E4EnRrkkiwLfzdk'; + credentials.expiration = Date.now() - 1; + + let response = yield xhr.send(req, 'http://127.0.0.1:1337', { + retry: false + }); + + assert.instanceOf(response, XMLHttpRequest); + assert.ok(refresh.isDone(), 'should refresh'); + assert.strictEqual(credentials.accessToken, 'Little Bear'); + assert.typeOf(credentials.expiration, 'number'); + assert.operator(credentials.expiration, '>', Date.now()); + assert.ok(mock.isDone(), 'should send req with Authorization header'); + })); + + test('should use provided access token if not expired', co.wrap(function *() { + let token = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply(500); + + let mock = nock('http://127.0.0.1:1337') + .get('/') + .matchHeader('Authorization', 'Bearer Little Bear') + .reply(200, '200 OK'); + + credentials.accessToken = 'Little Bear'; + credentials.refreshToken = 'spicy tamales'; + let expiration = credentials.expiration = Date.now() + 60 * 60 * 1000; + + let response = yield xhr.send(req, 'http://127.0.0.1:1337', { + retry: false + }); + + assert.instanceOf(response, XMLHttpRequest); + assert.notOk(token.isDone(), 'should not fetch new token(s)'); + assert.strictEqual(credentials.accessToken, 'Little Bear'); + assert.strictEqual(credentials.refreshToken, 'spicy tamales'); + assert.strictEqual(expiration, credentials.expiration); + assert.ok(mock.isDone()); + })); + + test('should retry if 401', co.wrap(function *() { + let refresh = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply( + 200, + JSON.stringify({ + access_token: 'Little Bear', + expires_in: 314159 + }) + ); + + let authorized = nock('http://127.0.0.1:1337') + .get('/') + .matchHeader('Authorization', 'Bearer Little Bear') + .reply(200, '200 OK'); + + let unauthorized = nock('http://127.0.0.1:1337') + .get('/') + .matchHeader('Authorization', 'Bearer EXPIRED') + .reply(401, '401 Unauthorized'); + + credentials.accessToken = 'EXPIRED'; + credentials.refreshToken = 'raspberry pie'; + credentials.expiration = Date.now() + 60 * 60 * 1000; + + let response = yield xhr.send(req, 'http://127.0.0.1:1337'); + assert.instanceOf(response, XMLHttpRequest); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.responseText, '200 OK'); + assert.ok(unauthorized.isDone(), 'tried to use expired access token'); + assert.ok(refresh.isDone(), 'should refresh access token on 401'); + assert.strictEqual(credentials.accessToken, 'Little Bear'); + assert.operator(credentials.expiration, '>', Date.now()); + assert.ok(authorized.isDone(), 'should then use new access token'); + })); + + test('should retry once at most', co.wrap(function *() { + let refresh = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply( + 200, + JSON.stringify({ + access_token: 'EXPIRED', + expires_in: 314159 + }) + ); + + nock('http://127.0.0.1:1337') + .persist() + .get('/') + .matchHeader('Authorization', 'Bearer EXPIRED') + .reply(401, '401 Unauthorized'); + + credentials.accessToken = 'EXPIRED'; + credentials.refreshToken = 'soda'; + + let spy = sinon.spy(xhr, 'send'); + + try { + yield xhr.send(req, 'http://127.0.0.1:1337') + assert.fail('Should have failed on error'); + } catch (error) { + assert.instanceOf(error, Error); + assert.include(error.toString(), 'Bad status: 401'); + assert.ok(refresh.isDone(), 'should refresh access token on 401'); + assert.strictEqual(spy.callCount, 2); + spy.restore(); + } + })); +}); diff --git a/js/dav/test/unit/xmlhttprequest_test.js b/js/dav/test/unit/xmlhttprequest_test.js new file mode 100644 index 00000000..027874e7 --- /dev/null +++ b/js/dav/test/unit/xmlhttprequest_test.js @@ -0,0 +1,81 @@ +import { assert } from 'chai'; +import co from 'co'; +import nock from 'nock'; +import sinon from 'sinon'; + +import { createSandbox } from '../../lib/sandbox'; +import XMLHttpRequest from '../../lib/xmlhttprequest'; + +suite('XMLHttpRequest#send', function() { + let request; + + setup(function() { + request = new XMLHttpRequest(); + }); + + teardown(function() { + nock.cleanAll(); + }); + + test('should sandbox request if provided', function() { + nock('http://127.0.0.1:1337').get('/'); + + request.open('GET', 'http://127.0.0.1:1337', true); + let sandbox = createSandbox(); + request.sandbox = sandbox; + let spy = sinon.spy(request, 'abort'); + request.send(); + sandbox.abort(); + sinon.assert.calledOnce(spy); + }); + + test('should send data if provided', co.wrap(function *() { + nock('http://127.0.0.1:1337') + .post('/', 'zippity-doo-dah') + .reply(200, 'zip-a-dee-a'); + + request.open('POST', 'http://127.0.0.1:1337', true); + let responseText = yield request.send('zippity-doo-dah'); + assert.strictEqual(responseText, 'zip-a-dee-a'); + })); + + test('should reject with statusText if status >=400', co.wrap(function *() { + nock('http://127.0.0.1:1337') + .get('/') + .reply(500, '500 Internal Server Error'); + + request.open('GET', 'http://127.0.0.1:1337', true); + try { + yield request.send(); + assert.fail('Did not reject promise on xhr error.'); + } catch (error) { + assert.instanceOf(error, Error); + }; + })); + + test('should reject with timeout error on timeout', co.wrap(function *() { + nock('http://127.0.0.1:1337') + .get('/') + .delay(2) + .reply(200, '200 OK'); + + request.timeout = 1; + request.open('GET', 'http://127.0.0.1:1337', true); + try { + yield request.send(); + assert.fail('Did not reject promise on timeout.'); + } catch (error) { + assert.instanceOf(error, Error); + } + })); + + test('should resolve with responseText if everything ok', co.wrap(function *() { + nock('http://127.0.0.1:1337') + .get('/') + .reply(200, '200 OK'); + + request.open('GET', 'http://127.0.0.1:1337', true); + let responseText = yield request.send(); + assert.strictEqual(responseText.trim(), '200 OK'); + })); +}); -- cgit v1.2.3