From b6eb18b23296fd0df57a0a85023d399437bd9376 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Tue, 3 Oct 2017 23:06:55 +0200 Subject: [PATCH] Migrate accounts+capabilities to new JMAP authentication spec According to the recent updates to the JMAP spec, account data is now loaded from the authentication response. Therefore the AuthAccess model was significantly changed to serve as a container for authentication data, account information and server capabilities. This change requires authentication responses to comply with the latest spec as the model requires certain new properties to be present. Added: jmap.ServerCapablities Removed: jmap.AccountCapabilities The namespace URIs used for the various capabilites are defined in jmap.Constants but not yet final. see links in issue #58 --- lib/API.js | 4 +- lib/client/Client.js | 31 +++- lib/models/Account.js | 37 ++-- lib/models/AccountCapabilities.js | 18 -- lib/models/AuthAccess.js | 48 +++++- lib/models/Capabilities.js | 16 +- lib/models/MailCapabilities.js | 13 +- lib/models/ServerCapabilities.js | 30 ++++ lib/utils/Constants.js | 4 +- test/common/Client.js | 179 +++++++++---------- test/common/models/Account.js | 37 ++-- test/common/models/AccountCapabilities.js | 24 --- test/common/models/AuthAccess.js | 200 ++++++++++++++++++++-- test/common/models/Capabilities.js | 13 +- test/common/models/MailCapabilities.js | 21 ++- test/common/models/ServerCapabilities.js | 42 +++++ 16 files changed, 495 insertions(+), 222 deletions(-) delete mode 100644 lib/models/AccountCapabilities.js create mode 100644 lib/models/ServerCapabilities.js delete mode 100644 test/common/models/AccountCapabilities.js create mode 100644 test/common/models/ServerCapabilities.js diff --git a/lib/API.js b/lib/API.js index 934ff51..ed20ea2 100644 --- a/lib/API.js +++ b/lib/API.js @@ -35,9 +35,9 @@ * @property AuthContinuation {AuthContinuation} The {@link AuthContinuation} class * @property Constants {Constants} The {@link module:Constants|Constants} object * @property Attachment {Attachment} The {@link Attachment} class - * @property AccountCapabilities {AccountCapabilities} The {@link AccountCapabilities} class * @property Capabilities {Capabilities} The {@link Capabilities} class * @property MailCapabilities {MailCapabilities} The {@link MailCapabilities} class + * @property ServerCapabilities {ServerCapabilities} The {@link ServerCapabilities} class * @property VacationResponse {VacationResponse} The {@link VacationResponse} class * @property TransportError {TransportError} The {@link TransportError} class * @property JmapError {JmapError} The {@link JmapError} class @@ -70,9 +70,9 @@ export default { AuthMethod: require('./models/AuthMethod'), Constants: require('./utils/Constants'), Attachment: require('./models/Attachment'), - AccountCapabilities: require('./models/AccountCapabilities'), Capabilities: require('./models/Capabilities'), MailCapabilities: require('./models/MailCapabilities'), + ServerCapabilities: require('./models/ServerCapabilities'), VacationResponse: require('./models/VacationResponse'), TransportError: require('./errors/TransportError'), JmapError: require('./errors/JmapError') diff --git a/lib/client/Client.js b/lib/client/Client.js index d0a1ee0..7aa6cd2 100644 --- a/lib/client/Client.js +++ b/lib/client/Client.js @@ -103,16 +103,22 @@ export default class Client { *
* The individual properties of the AuthAccess object will be copied into client properties. * - * @param access {AuthAccess} The response object from an authenticate process. + * @param access {AuthAccess|Object} The response object from an authenticate process. * * @return {Client} This Client instance. */ withAuthAccess(access) { - Utils.assertRequiredParameterHasType(access, 'access', AuthAccess); + Utils.assertRequiredParameterIsObject(access, 'access'); + // create an instance of AuthAccess if plain object is given + if (!(access instanceof AuthAccess)) { + access = new AuthAccess(this, access); + } + + this.authAccess = access; this.authScheme = 'X-JMAP'; this.authToken = access.accessToken; - ['username', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'versions', 'extensions'].forEach((property) => { + ['username', 'signingId', 'signingKey', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'serverCapabilities', 'mailCapabilities'].forEach((property) => { this[property] = access[property]; }); @@ -182,7 +188,7 @@ export default class Client { .then(resp => this._authenticateResponse(resp, continuationCallback)); } else if (data.accessToken && data.loginId === undefined) { // got auth access response - return new AuthAccess(data); + return new AuthAccess(this, data); } else { // got unknown response data throw new Error('Unexpected response on authorization request'); @@ -250,6 +256,23 @@ export default class Client { * @see http://jmap.io/spec.html#getaccounts */ getAccounts(options) { + // resolve with accounts list from AuthAccess + if (this.authAccess instanceof AuthAccess) { + return this.promiseProvider.newPromise(function(resolve, reject) { + let accounts = []; + + // equivalent to Object.values() + for (let id in this.authAccess.accounts) { + if (this.authAccess.accounts.hasOwnProperty(id)) { + accounts.push(this.authAccess.accounts[id]); + } + } + + resolve(accounts); + }.bind(this)); + } + + // fallback to deprecated getAccounts request return this._jmapRequest('getAccounts', options); } diff --git a/lib/models/Account.js b/lib/models/Account.js index a33ce32..b9cda47 100644 --- a/lib/models/Account.js +++ b/lib/models/Account.js @@ -2,9 +2,7 @@ import Model from './Model.js'; import Utils from '../utils/Utils.js'; -import Capabilities from './Capabilities'; -import MailCapabilities from './MailCapabilities'; -import AccountCapabilities from './AccountCapabilities'; +import JSONBuilder from '../utils/JSONBuilder.js'; export default class Account extends Model { /** @@ -18,10 +16,9 @@ export default class Account extends Model { * @param [opts] {Object} The optional properties of this _Account_. * @param [opts.name=''] {String} The name of this _Account_. * @param [opts.isPrimary=false] {Boolean} Whether this _Account_ is the primary email account. - * @param [opts.capabilities=null] {AccountCapabilities} An object describing general capabilities of this server. - * @param [opts.mail=null] {MailCapabilities} If null, this account does not support mail. Otherwise, the {MailCapabilities} of the server. - * @param [opts.contacts=null] {Capabilities} If null, this account does not support contacts. Otherwise, the contacts {Capabilities} of the server. - * @param [opts.calendars=null] {Capabilities} If null, this account does not support calendars. Otherwise, the calendars {Capabilities} of the server. + * @param [opts.isReadOnly=false] {Boolean} Whether the entire _Account_ is read-only + * @param [opts.hasDataFor=[]] {String[]} A list of the data profiles available in this account +server. * * @see Model */ @@ -35,10 +32,8 @@ export default class Account extends Model { this.id = id; this.name = opts.name || ''; this.isPrimary = opts.isPrimary || false; - this.capabilities = Utils._nullOrNewInstance(opts.capabilities, AccountCapabilities); - this.mail = Utils._nullOrNewInstance(opts.mail, MailCapabilities); - this.contacts = Utils._nullOrNewInstance(opts.contacts, Capabilities); - this.calendars = Utils._nullOrNewInstance(opts.calendars, Capabilities); + this.isReadOnly = opts.isReadOnly || false; + this.hasDataFor = opts.hasDataFor || []; } /** @@ -47,7 +42,7 @@ export default class Account extends Model { * @returns {Boolean} _true_ if and only if this `Account` has mail enabled, _false_ otherwise. */ hasMail() { - return this.mail !== null; + return this.hasDataFor.indexOf('mail') >= 0; } /** @@ -56,7 +51,7 @@ export default class Account extends Model { * @returns {Boolean} _true_ if and only if this `Account` has calendars enabled, _false_ otherwise. */ hasCalendars() { - return this.calendars !== null; + return this.hasDataFor.indexOf('calendars') >= 0; } /** @@ -65,7 +60,7 @@ export default class Account extends Model { * @returns {Boolean} _true_ if and only if this `Account` has contacts enabled, _false_ otherwise. */ hasContacts() { - return this.contacts !== null; + return this.hasDataFor.indexOf('contacts') >= 0; } /** @@ -86,6 +81,20 @@ export default class Account extends Model { return this._jmap.getMailboxes(options); } + /** + * Creates a JSON representation from this {@link Account}. + * + * @return JSON object with only owned properties and non-null default values. + */ + toJSONObject() { + return new JSONBuilder() + .append('name', this.name) + .append('isPrimary', this.isPrimary) + .append('isReadOnly', this.isReadOnly) + .append('hasDataFor', this.hasDataFor) + .build(); + } + /** * Creates an _Account_ from its JSON representation. * diff --git a/lib/models/AccountCapabilities.js b/lib/models/AccountCapabilities.js deleted file mode 100644 index 3ecf313..0000000 --- a/lib/models/AccountCapabilities.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -export default class AccountCapabilities { - /** - * This class represents a JMAP [AccountCapabilities]{@link http://jmap.io/spec.html#accounts*}.
- * An _AccountCapabilities_ object describes general capabilities of a JMAP server. - * - * @constructor - * - * @param [opts] {Object} The optional properties of this _AccountCapabilities_. - * @param [opts.maxSizeUpload=0] {Number} The maximum file size, in bytes, that the server will accept for a single file upload. - */ - constructor(opts) { - opts = opts || {}; - - this.maxSizeUpload = opts.maxSizeUpload || 0; - } -} diff --git a/lib/models/AuthAccess.js b/lib/models/AuthAccess.js index 2b1dab0..821483d 100644 --- a/lib/models/AuthAccess.js +++ b/lib/models/AuthAccess.js @@ -1,29 +1,65 @@ 'use strict'; +import Model from './Model.js'; import Utils from '../utils/Utils.js'; +import Constants from '../utils/Constants.js'; +import Account from './Account.js'; +import ServerCapabilities from './ServerCapabilities.js'; +import MailCapabilities from './MailCapabilities.js'; +import JSONBuilder from '../utils/JSONBuilder.js'; -export default class AuthAccess { +export default class AuthAccess extends Model { /** - * This class represents a JMAP [Auth Access Response]{@link http://jmap.io/spec.html#authentication}. + * This class represents a JMAP [Auth Access Response]{@link http://jmap.io/spec-core.html#201-authentication-is-complete-access-token-created}. * * @constructor * + * @param jmap {Client} The {@link Client} instance that created this _AuthAccess_. * @param payload {Object} The server response of an auth access request. */ + constructor(jmap, payload) { + super(jmap); - constructor(payload) { Utils.assertRequiredParameterIsPresent(payload, 'payload'); - ['username', 'accessToken', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl'].forEach((property) => { + ['username', 'accessToken', 'signingId', 'signingKey', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'accounts', 'capabilities'].forEach((property) => { Utils.assertRequiredParameterIsPresent(payload[property], property); }); this.username = payload.username; - this.versions = payload.versions || []; - this.extensions = payload.extensions || {}; this.accessToken = payload.accessToken; + this.signingId = payload.signingId; + this.signingKey = payload.signingKey; this.apiUrl = payload.apiUrl; this.eventSourceUrl = payload.eventSourceUrl; this.uploadUrl = payload.uploadUrl; this.downloadUrl = payload.downloadUrl; + this.capabilities = payload.capabilities; + this.serverCapabilities = new ServerCapabilities(this.capabilities[Constants.CORE_CAPABILITIES_URI] || {}); + this.mailCapabilities = new MailCapabilities(this.capabilities[Constants.MAIL_CAPABILITIES_URI] || {}); + + this.accounts = {}; + for (var accountId in payload.accounts) { + this.accounts[accountId] = Account.fromJSONObject(jmap, Object.assign({ id: accountId }, payload.accounts[accountId])); + } + } + + /** + * Creates a JSON representation from this {@link AuthAccess}. + * + * @return JSON object with only owned properties and non-null default values. + */ + toJSONObject() { + return new JSONBuilder() + .append('username', this.username) + .append('accessToken', this.accessToken) + .append('signingId', this.signingId) + .append('signingKey', this.signingKey) + .append('apiUrl', this.apiUrl) + .append('eventSourceUrl', this.eventSourceUrl) + .append('uploadUrl', this.uploadUrl) + .append('downloadUrl', this.downloadUrl) + .appendObject('accounts', this.accounts) + .appendObject('capabilities', this.capabilities) + .build(); } } diff --git a/lib/models/Capabilities.js b/lib/models/Capabilities.js index 01128ef..c94d959 100644 --- a/lib/models/Capabilities.js +++ b/lib/models/Capabilities.js @@ -1,16 +1,20 @@ 'use strict'; +import Utils from '../utils/Utils.js'; + export default class Capabilities { /** - * This class represents an abstract JMAP "capabilities" object. See {@link http://jmap.io/spec.html#accounts*}.
- * It is subclassed in more specific xxxCapabilities classes. + * This class represents an generic JMAP "capabilities" object. See {@link http://jmap.io/spec-core.html#getting-an-access-token*}.
* - * @param [opts] {Object} The optional properties of this _AccountCapabilities_. - * @param [opts.isReadOnly=false] {Boolean} Whether the feature denoted by this _Capabilities_ instance is read-only. + * @param namespace {String} The namespace/identifier of the capabilities. + * @param [opts] {Object} The optional properties of this _Capabilities_. */ - constructor(opts) { + constructor(namespace, opts) { + Utils.assertRequiredParameterIsPresent(namespace, 'namespace'); + opts = opts || {}; - this.isReadOnly = !!opts.isReadOnly; + this.ns = namespace; + Object.assign(this, opts); } } diff --git a/lib/models/MailCapabilities.js b/lib/models/MailCapabilities.js index e284b8e..833dfe4 100644 --- a/lib/models/MailCapabilities.js +++ b/lib/models/MailCapabilities.js @@ -1,6 +1,7 @@ 'use strict'; import Capabilities from './Capabilities'; +import Constants from '../utils/Constants'; export default class MailCapabilities extends Capabilities { /** @@ -11,19 +12,23 @@ export default class MailCapabilities extends Capabilities { * @extends Capabilities * * @param [opts] {Object} The optional properties of this _MailCapabilities_. + * @param [opts.maxMailboxesPerMessage=null] {Number} The maximum number of mailboxes that can be can assigned to a single message. * @param [opts.maxSizeMessageAttachments=0] {Number} The maximum total size of attachments, in bytes, allowed for messages. - * @param [opts.canDelaySend=false] {Boolean} Whether the server supports inserting a message into the outbox to be sent later. - * @param [opts.messageListSortOptions=0] {String[]} A list of all the message properties the server supports for sorting by. + * @param [opts.maxDelayedSend=0] {Number} The number in seconds of the maximum delay the server supports in sending. 0 if delayed send is not supported. + * @param [opts.messageListSortOptions=[]] {String[]} A list of all the message properties the server supports for sorting by. + * @param [opts.submissionExtensions={} {String[String[]]} Known safe-to-expose EHLO capabilities of the submission server. * * @see Capabilities */ constructor(opts) { opts = opts || {}; - super(opts); + super(Constants.MAIL_CAPABILITIES_URI); + this.maxMailboxesPerMessage = opts.maxMailboxesPerMessage || null; this.maxSizeMessageAttachments = opts.maxSizeMessageAttachments || 0; - this.canDelaySend = !!opts.canDelaySend; + this.maxDelayedSend = opts.maxDelayedSend || 0; this.messageListSortOptions = opts.messageListSortOptions || []; + this.submissionExtensions = opts.submissionExtensions || {}; } } diff --git a/lib/models/ServerCapabilities.js b/lib/models/ServerCapabilities.js new file mode 100644 index 0000000..9f58be5 --- /dev/null +++ b/lib/models/ServerCapabilities.js @@ -0,0 +1,30 @@ +'use strict'; + +export default class ServerCapabilities { + /** + * This class represents a JMAP [ServerCapabilities]{@link http://jmap.io/spec-core.html#201-authentication-is-complete-access-token-created}.
+ * An _ServerCapabilities_ object describes general capabilities of a JMAP server. + * + * @constructor + * + * @param [opts] {Object} The optional properties of this _ServerCapabilities_. + * @param [opts.maxSizeUpload=0] {Number} The maximum file size, in bytes, that the server will accept for a single file upload. + * @param [opts.maxSizeRequest=0] {Number} The maximum size, in bytes, that the server will accept for a single request to the API endpoint. + * @param [opts.maxConcurrentUpload=1] {Number} The maximum number of concurrent requests the server will accept to the upload endpoint. + * @param [opts.maxConcurrentRequests=1] {Number} The maximum number of concurrent requests the server will accept to the API endpoint. + * @param [opts.maxCallsInRequest=1] {Number} The maximum number of method calls the server will accept in a single request to the API endpoint. + * @param [opts.maxObjectsInGet=0] {Number} The maximum number of objects that the client may request in a single getXXX type method call. + * @param [opts.maxObjectsInSet=0] {Number} The maximum number of objects the client may send to create, update or destroy in a single setXXX type method call. + */ + constructor(opts) { + opts = opts || {}; + + this.maxSizeUpload = opts.maxSizeUpload || 0; + this.maxSizeRequest = opts.maxSizeRequest || 0; + this.maxConcurrentUpload = opts.maxConcurrentUpload || 1; + this.maxConcurrentRequests = opts.maxConcurrentRequests || 1; + this.maxCallsInRequest = opts.maxCallsInRequest || 1; + this.maxObjectsInGet = opts.maxObjectsInGet || 0; + this.maxObjectsInSet = opts.maxObjectsInSet || 0; + } +} diff --git a/lib/utils/Constants.js b/lib/utils/Constants.js index 583a837..9cf7e33 100644 --- a/lib/utils/Constants.js +++ b/lib/utils/Constants.js @@ -11,5 +11,7 @@ export default { VERSION: '__VERSION__', CLIENT_NAME: 'jmap-client (https://github.com/linagora/jmap-client)', SUCCESS_RESPONSE_CODES: [200, 201], - ERROR: 'error' + ERROR: 'error', + CORE_CAPABILITIES_URI: 'http://jmap.io/spec-core.html', + MAIL_CAPABILITIES_URI: 'http://jmap.io/spec-mail.html' }; diff --git a/test/common/Client.js b/test/common/Client.js index 6a60bc0..fc95107 100644 --- a/test/common/Client.js +++ b/test/common/Client.js @@ -10,6 +10,19 @@ require('chai').use(require('sinon-chai')); describe('The Client class', function() { + var authAccessResponse = { + username: 'user@domain.com', + signingId: 'signId1', + signingKey: 'signKeyA', + accessToken: 'accessToken1', + apiUrl: '/', + eventSourceUrl: '/es', + uploadUrl: '/upload', + downloadUrl: '/download', + accounts: {}, + capabilities: {} + }; + function defaultClient() { return new jmap.Client({ post: function() { @@ -75,18 +88,20 @@ describe('The Client class', function() { describe('The withAuthAccess method', function() { - var authAccess = new jmap.AuthAccess({ + var authAccess = new jmap.AuthAccess(defaultClient(), { username: 'user', - versions: [1], - extensions: { 'com.linagory.ext': [1] }, accessToken: 'accessToken1', + signingId: 'signId1', + signingKey: 'signKeyA', apiUrl: '/es', eventSourceUrl: '/eventSource', uploadUrl: '/upload', - downloadUrl: '/download' + downloadUrl: '/download', + accounts: {}, + capabilities: { 'com.linagory.ext': {} } }); - it('should throw if access parameter is not an instance of AuthAccess', function() { + it('should throw if access parameter is invalid', function() { expect(function() { defaultClient().withAuthAccess({}); }).to.throw(Error); @@ -97,7 +112,18 @@ describe('The Client class', function() { expect(client.authToken).to.equal(authAccess.accessToken); expect(client.authScheme).to.equal('X-JMAP'); - ['username', 'versions', 'extensions', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl'].forEach(function(property) { + ['username', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'signingId', 'signingKey'].forEach(function(property) { + expect(client[property]).to.equal(authAccess[property]); + }); + expect(client.serverCapabilities).to.be.an.instanceof(jmap.ServerCapabilities); + expect(client.mailCapabilities).to.be.an.instanceof(jmap.MailCapabilities); + }); + + it('should accept a plain json object', function() { + var client = defaultClient().withAuthAccess(authAccess.toJSONObject()); + + expect(client.authToken).to.equal(authAccess.accessToken); + ['username', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'signingId', 'signingKey'].forEach(function(property) { expect(client[property]).to.equal(authAccess[property]); }); }); @@ -106,6 +132,19 @@ describe('The Client class', function() { describe('The getAccounts method', function() { + function defaultAccounts(id) { + var accounts = {}; + + accounts[id] = { + name: 'test', + isPrimary: true, + isReadOnly: false, + hasDataFor: ['mail'] + }; + + return accounts; + } + it('should post on the API url', function(done) { new jmap.Client({ post: function(url) { @@ -156,32 +195,6 @@ describe('The Client class', function() { .then(null, done); }); - it('should send correct HTTP Authorization header after JMAP authentication', function(done) { - var authAccess = new jmap.AuthAccess({ - username: 'user', - accessToken: 'accessToken1', - apiUrl: 'https://test', - eventSourceUrl: '/es', - uploadUrl: '/upload', - downloadUrl: '/download' - }); - - new jmap.Client({ - post: function(url, headers) { - expect(headers).to.deep.equal({ - Authorization: 'X-JMAP accessToken1', - 'Content-Type': 'application/json; charset=UTF-8', - Accept: 'application/json; charset=UTF-8' - }); - - return q.reject(); - } - }) - .withAuthAccess(authAccess) - .getAccounts() - .then(null, done); - }); - it('should send a valid JMAP "getAccounts" request body when there is no options', function(done) { new jmap.Client({ post: function(url, headers, body) { @@ -235,14 +248,47 @@ describe('The Client class', function() { .getAccounts() .then(function(data) { expect(data).to.deep.equal([ - new jmap.Account(client, 'id'), - new jmap.Account(client, 'id2') + new jmap.Account(client, 'id', {}), + new jmap.Account(client, 'id2', {}) ]); done(); }); }); + it('returns account information received during JMAP authentication', function(done) { + var authAccess = { + username: 'user', + accessToken: 'accessToken1', + signingId: 'signId1', + signingKey: 'signKeyA', + apiUrl: 'https://test', + eventSourceUrl: '/es', + uploadUrl: '/upload', + downloadUrl: '/download', + accounts: defaultAccounts('a1'), + capabilities: {} + }; + + var client = new jmap.Client({ + post: function(url, headers) { + return q.reject(); + } + }, new jmap.QPromiseProvider()); + + client + .withAuthAccess(authAccess) + .getAccounts() + .then(function(data) { + expect(data).to.deep.equal([ + new jmap.Account(client, 'a1', authAccess.accounts.a1) + ]); + + done(); + }) + .catch(done); + }); + }); describe('The getMailboxes method', function() { @@ -1809,17 +1855,6 @@ describe('The Client class', function() { }); it('should resolve with AuthAccess', function(done) { - var authAccessResponse = { - username: 'user@domain.com', - versions: [1], - extensions: {}, - accessToken: 'accessToken1', - apiUrl: '/', - eventSourceUrl: '/es', - uploadUrl: '/upload', - downloadUrl: '/download' - }; - new jmap.Client({ post: function(url, headers, data) { if (data.username) { @@ -1837,23 +1872,13 @@ describe('The Client class', function() { return q({ type: 'external' }); }) .then(function(authAccess) { - expect(authAccess).to.deep.equal(authAccessResponse); + expect(authAccess.toJSONObject()).to.deep.equal(authAccessResponse); done(); - }); + }) + .catch(done); }); it('should repeat authentication steps if server demands to', function(done) { - var authAccessResponse = { - username: 'user@domain.com', - versions: [1], - extensions: {}, - accessToken: 'accessToken1', - apiUrl: '/', - eventSourceUrl: '/es', - uploadUrl: '/upload', - downloadUrl: '/download' - }; - new jmap.Client({ post: function(url, headers, data) { if (data.username) { @@ -1889,9 +1914,10 @@ describe('The Client class', function() { } }) .then(function(authAccess) { - expect(authAccess).to.deep.equal(authAccessResponse); + expect(authAccess.toJSONObject()).to.deep.equal(authAccessResponse); done(); - }, done); + }) + .catch(done); }); it('should reject on ambiguous server response', function(done) { @@ -1941,6 +1967,7 @@ describe('The Client class', function() { }); describe('The authExternal method', function() { + it('should reject if the server does not support external authentication', function(done) { new jmap.Client({ post: function(url, headers, data) { @@ -1978,17 +2005,6 @@ describe('The Client class', function() { }); it('should give back a AuthAccess', function(done) { - var authAccessResponse = { - username: 'user@domain.com', - versions: [1], - extensions: {}, - accessToken: 'accessToken1', - apiUrl: '/', - eventSourceUrl: '/es', - uploadUrl: '/upload', - downloadUrl: '/download' - }; - new jmap.Client({ post: function(url, headers, data) { if (data.username) { @@ -2006,9 +2022,10 @@ describe('The Client class', function() { return q(authContinuation.loginId); }) .then(function(authAccess) { - expect(authAccess).to.deep.equal(authAccessResponse); + expect(authAccess.toJSONObject()).to.deep.equal(authAccessResponse); done(); - }); + }) + .catch(done); }); }); @@ -2034,17 +2051,6 @@ describe('The Client class', function() { }); it('should give back a AuthAccess', function(done) { - var authAccessResponse = { - username: 'user@domain.com', - versions: [1], - extensions: {}, - accessToken: 'accessToken1', - apiUrl: '/', - eventSourceUrl: '/es', - uploadUrl: '/upload', - downloadUrl: '/download' - }; - new jmap.Client({ post: function(url, headers, data) { if (data.username) { @@ -2060,9 +2066,10 @@ describe('The Client class', function() { .withAuthenticationUrl('https://test') .authPassword('user@domain.com', 'xxxxxx', 'Device name') .then(function(authAccess) { - expect(authAccess).to.deep.equal(authAccessResponse); + expect(authAccess.toJSONObject()).to.deep.equal(authAccessResponse); done(); - }); + }) + .catch(done); }); }); diff --git a/test/common/models/Account.js b/test/common/models/Account.js index c43c338..de64bea 100644 --- a/test/common/models/Account.js +++ b/test/common/models/Account.js @@ -11,10 +11,8 @@ describe('The Account class', function() { id: id, name: '', isPrimary: false, - capabilities: null, - mail: null, - contacts: null, - calendars: null + isReadOnly: false, + hasDataFor: [] }; } @@ -38,21 +36,8 @@ describe('The Account class', function() { var expectedAccount = { name: 'name', isPrimary: true, - capabilities: { - maxSizeUpload: 123 - }, - mail: { - isReadOnly: false, - maxSizeMessageAttachments: 456, - canDelaySend: true, - messageListSortOptions: ['date'] - }, - contacts: { - isReadOnly: true - }, - calendars: { - isReadOnly: true - } + isReadOnly: false, + hasDataFor: ['mail', 'contacts', 'calendars'] }; var account = new jmap.Account({}, 'id', expectedAccount); @@ -119,16 +104,14 @@ describe('The Account class', function() { id: 'id', name: 'name', isPrimary: true, - calendars: { - isReadOnly: true - } + hasDataFor: ['calendars'] }); expect(account.id).to.equal('id'); expect(account.name).to.equal('name'); expect(account.isPrimary).to.equal(true); - expect(account.calendars).to.deep.equal({ isReadOnly: true }); - expect(account.mail).to.equal(null); + expect(account.hasDataFor).to.deep.equal(['calendars']); + expect(account.isReadOnly).to.equal(false); }); }); @@ -140,7 +123,7 @@ describe('The Account class', function() { }); it('should return true when the account has mail capabilities defined', function() { - expect(new jmap.Account({}, 'id', { mail: {} }).hasMail()).to.equal(true); + expect(new jmap.Account({}, 'id', { hasDataFor: ['mail'] }).hasMail()).to.equal(true); }); }); @@ -152,7 +135,7 @@ describe('The Account class', function() { }); it('should return true when the account has calendars capabilities defined', function() { - expect(new jmap.Account({}, 'id', { calendars: {} }).hasCalendars()).to.equal(true); + expect(new jmap.Account({}, 'id', { hasDataFor: ['calendars'] }).hasCalendars()).to.equal(true); }); }); @@ -164,7 +147,7 @@ describe('The Account class', function() { }); it('should return true when the account has contacts capabilities defined', function() { - expect(new jmap.Account({}, 'id', { contacts: {} }).hasContacts()).to.equal(true); + expect(new jmap.Account({}, 'id', { hasDataFor: ['contacts'] }).hasContacts()).to.equal(true); }); }); diff --git a/test/common/models/AccountCapabilities.js b/test/common/models/AccountCapabilities.js deleted file mode 100644 index bb90b0e..0000000 --- a/test/common/models/AccountCapabilities.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -var expect = require('chai').expect, - jmap = require('../../../dist/jmap-client'); - -describe('The AccountCapabilities class', function() { - - describe('The constructor', function() { - - it('should use default values if opts is not defined', function() { - expect(new jmap.AccountCapabilities().maxSizeUpload).to.equal(0); - }); - - it('should use default values if an empty opts object is given', function() { - expect(new jmap.AccountCapabilities({}).maxSizeUpload).to.equal(0); - }); - - it('should allow defining values through the opts object', function() { - expect(new jmap.AccountCapabilities({ maxSizeUpload: 1234 }).maxSizeUpload).to.equal(1234); - }); - - }); - -}); diff --git a/test/common/models/AuthAccess.js b/test/common/models/AuthAccess.js index 205df9a..cbcb753 100644 --- a/test/common/models/AuthAccess.js +++ b/test/common/models/AuthAccess.js @@ -4,6 +4,19 @@ var expect = require('chai').expect, jmap = require('../../../dist/jmap-client'); describe('The AuthAccess class', function() { + function defaultAccounts(id) { + var accounts = {}; + + accounts[id] = { + name: 'test', + isPrimary: true, + isReadOnly: false, + hasDataFor: ['mail'] + }; + + return accounts; + } + describe('constructor', function() { it('should throw if payload parameter is not defined', function() { expect(function() { @@ -21,7 +34,7 @@ describe('The AuthAccess class', function() { }; expect(function() { - new jmap.AuthAccess(payload); + new jmap.AuthAccess({}, payload); }).to.throw(Error); }); @@ -31,11 +44,15 @@ describe('The AuthAccess class', function() { accessToken: 'http://localhost:8899', eventSourceUrl: 'http://localhost:8899/eventSource', uploadUrl: 'http://localhost:8899/upload', - downloadUrl: 'http://localhost:8899/download' + downloadUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + accounts: defaultAccounts('a1'), + capabilities: {} }; expect(function() { - new jmap.AuthAccess(payload); + new jmap.AuthAccess({}, payload); }).to.throw(Error); }); @@ -45,11 +62,15 @@ describe('The AuthAccess class', function() { accessToken: 'http://localhost:8899', apiUrl: 'http://localhost:8899/eventSource', uploadUrl: 'http://localhost:8899/upload', - downloadUrl: 'http://localhost:8899/download' + downloadUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + accounts: defaultAccounts('a1'), + capabilities: {} }; expect(function() { - new jmap.AuthAccess(payload); + new jmap.AuthAccess({}, payload); }).to.throw(Error); }); @@ -59,11 +80,15 @@ describe('The AuthAccess class', function() { accessToken: 'http://localhost:8899', apiUrl: 'http://localhost:8899/eventSource', eventSourceUrl: 'http://localhost:8899/upload', - downloadUrl: 'http://localhost:8899/download' + downloadUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + accounts: defaultAccounts('a1'), + capabilities: {} }; expect(function() { - new jmap.AuthAccess(payload); + new jmap.AuthAccess({}, payload); }).to.throw(Error); }); @@ -73,11 +98,15 @@ describe('The AuthAccess class', function() { accessToken: 'http://localhost:8899', apiUrl: 'http://localhost:8899/eventSource', uploadUrl: 'http://localhost:8899/upload', - eventSourceUrl: 'http://localhost:8899/download' + eventSourceUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + accounts: defaultAccounts('a1'), + capabilities: {} }; expect(function() { - new jmap.AuthAccess(payload); + new jmap.AuthAccess({}, payload); }).to.throw(Error); }); @@ -87,31 +116,168 @@ describe('The AuthAccess class', function() { apiUrl: 'http://localhost:8899/eventSource', uploadUrl: 'http://localhost:8899/upload', downloadUrl: 'http://localhost:8899/download', - eventSourceUrl: 'http://localhost:8899/download' + eventSourceUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + accounts: defaultAccounts('a1'), + capabilities: {} + }; + + expect(function() { + new jmap.AuthAccess({}, payload); + }).to.throw(Error); + }); + + it('should throw if payload.signingId parameter is not defined', function() { + var payload = { + username: 'user', + accessToken: 'http://localhost:8899', + apiUrl: 'http://localhost:8899/eventSource', + uploadUrl: 'http://localhost:8899/upload', + downloadUrl: 'http://localhost:8899/download', + eventSourceUrl: 'http://localhost:8899/download', + signingKey: 'signKeyA', + accounts: defaultAccounts('a1'), + capabilities: {} + }; + + expect(function() { + new jmap.AuthAccess({}, payload); + }).to.throw(Error); + }); + + it('should throw if payload.signingKey parameter is not defined', function() { + var payload = { + username: 'user', + accessToken: 'http://localhost:8899', + apiUrl: 'http://localhost:8899/eventSource', + uploadUrl: 'http://localhost:8899/upload', + downloadUrl: 'http://localhost:8899/download', + eventSourceUrl: 'http://localhost:8899/download', + signingId: 'signId1', + accounts: defaultAccounts('a1'), + capabilities: {} }; expect(function() { - new jmap.AuthAccess(payload); + new jmap.AuthAccess({}, payload); }).to.throw(Error); }); - it('should expose username, accessToken, apiUrl, eventSourceUrl, downloadUrl and uploadUrl properties', function() { + it('should throw if payload.accounts parameter is not defined', function() { + var payload = { + username: 'user', + accessToken: 'http://localhost:8899', + apiUrl: 'http://localhost:8899/eventSource', + uploadUrl: 'http://localhost:8899/upload', + downloadUrl: 'http://localhost:8899/download', + eventSourceUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + capabilities: {} + }; + + expect(function() { + new jmap.AuthAccess({}, payload); + }).to.throw(Error); + }); + + it('should throw if payload.capabilities parameter is not defined', function() { + var payload = { + username: 'user', + accessToken: 'http://localhost:8899', + apiUrl: 'http://localhost:8899/eventSource', + uploadUrl: 'http://localhost:8899/upload', + downloadUrl: 'http://localhost:8899/download', + eventSourceUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + accounts: defaultAccounts('a1') + }; + + expect(function() { + new jmap.AuthAccess({}, payload); + }).to.throw(Error); + }); + + it('should expose username, accessToken, signingId, signingKey, apiUrl, eventSourceUrl, downloadUrl, uploadUrl, accounts and capabilities properties', function() { var payload = { username: 'user', - versions: [1], - extensions: { 'com.fastmail.message': [1] }, accessToken: 'http://localhost:8899', apiUrl: 'http://localhost:8899/eventSource', eventSourceUrl: 'http://localhost:8899/eventSource', uploadUrl: 'http://localhost:8899/upload', - downloadUrl: 'http://localhost:8899/download' + downloadUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + accounts: defaultAccounts('account1'), + capabilities: { + 'com.fastmail.message': {}, + } + }; + + payload.capabilities[jmap.Constants.CORE_CAPABILITIES_URI] = { + maxSizeUpload: 2048, + maxSizeRequest: 4096, + }; + + payload.capabilities[jmap.Constants.MAIL_CAPABILITIES_URI] = { + maxSizeMessageAttachments: 2048, + maxDelayedSend: 600, }; - var authToken = new jmap.AuthAccess(payload); + var authToken = new jmap.AuthAccess({}, payload); - ['username', 'accessToken', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl'].forEach(function(property) { + ['username', 'accessToken', 'signingId', 'signingKey', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl'].forEach(function(property) { expect(authToken[property]).to.equal(payload[property]); }); + + expect(authToken).to.have.property('accounts'); + expect(authToken.accounts).to.be.an.instanceof(Object); + expect(authToken.accounts.account1).to.be.an.instanceof(jmap.Account); + + expect(authToken).to.have.property('serverCapabilities'); + expect(authToken.serverCapabilities).to.be.an.instanceof(jmap.ServerCapabilities); + expect(authToken.serverCapabilities.maxSizeRequest).to.equal(4096); + expect(authToken.serverCapabilities.maxSizeUpload).to.equal(2048); + + expect(authToken).to.have.property('mailCapabilities'); + expect(authToken.mailCapabilities).to.be.an.instanceof(jmap.MailCapabilities); + expect(authToken.mailCapabilities.maxSizeMessageAttachments).to.equal(2048); + expect(authToken.mailCapabilities.maxDelayedSend).to.equal(600); + }); + }); + + describe('The toJSONObject method', function() { + + it('should replicate the submitted payload', function() { + var payload = { + username: 'user', + accessToken: 'http://localhost:8899', + apiUrl: 'http://localhost:8899/eventSource', + eventSourceUrl: 'http://localhost:8899/eventSource', + uploadUrl: 'http://localhost:8899/upload', + downloadUrl: 'http://localhost:8899/download', + signingId: 'signId1', + signingKey: 'signKeyA', + accounts: defaultAccounts('account1'), + capabilities: {} + }; + + payload.capabilities[jmap.Constants.CORE_CAPABILITIES_URI] = { + maxCallsInRequest: 1, + maxConcurrentRequests: 2, + maxConcurrentUpload: 2, + maxObjectsInGet: 100, + maxObjectsInSet: 100, + maxSizeRequest: 1024, + maxSizeUpload: 1024, + }; + + var authAccess = new jmap.AuthAccess({}, payload); + + expect(authAccess.toJSONObject()).to.deep.equal(payload); }); + }); }); diff --git a/test/common/models/Capabilities.js b/test/common/models/Capabilities.js index cac3b76..4cb5ec6 100644 --- a/test/common/models/Capabilities.js +++ b/test/common/models/Capabilities.js @@ -6,17 +6,20 @@ var expect = require('chai').expect, describe('The Capabilities class', function() { describe('The constructor', function() { + var namespace = 'com.linagora.jmap.ext1'; - it('should use default values if opts is not defined', function() { - expect(new jmap.Capabilities().isReadOnly).to.equal(false); + it('should throw an Error if namespace parameter is not defined', function() { + expect(function() { + new jmap.Capabilities(); + }).to.throw(Error); }); - it('should use default values if an empty opts object is given', function() { - expect(new jmap.Capabilities({}).isReadOnly).to.equal(false); + it('should store namespace parameter', function() { + expect(new jmap.Capabilities(namespace).ns).to.equal(namespace); }); it('should allow defining values through the opts object', function() { - expect(new jmap.Capabilities({ isReadOnly: true }).isReadOnly).to.equal(true); + expect(new jmap.Capabilities(namespace, { isReadOnly: true }).isReadOnly).to.equal(true); }); }); diff --git a/test/common/models/MailCapabilities.js b/test/common/models/MailCapabilities.js index 8562611..1c179b6 100644 --- a/test/common/models/MailCapabilities.js +++ b/test/common/models/MailCapabilities.js @@ -6,10 +6,12 @@ var expect = require('chai').expect, describe('The MailCapabilities class', function() { var defaultMailCapabilities = { - isReadOnly: false, + ns: jmap.Constants.MAIL_CAPABILITIES_URI, + maxMailboxesPerMessage: null, maxSizeMessageAttachments: 0, - canDelaySend: false, - messageListSortOptions: [] + maxDelayedSend: 0, + messageListSortOptions: [], + submissionExtensions: {} }; describe('The constructor', function() { @@ -24,16 +26,19 @@ describe('The MailCapabilities class', function() { it('should allow defining values through the opts object', function() { var capabilities = new jmap.MailCapabilities({ - isReadOnly: true, + maxMailboxesPerMessage: 8, maxSizeMessageAttachments: 1234, - canDelaySend: true, - messageListSortOptions: ['date', 'id'] + maxDelayedSend: 120, + messageListSortOptions: ['date', 'id'], + submissionExtensions: { DSN: ['RET=HDRS'] } }); - expect(capabilities.isReadOnly).to.equal(true); + expect(capabilities.ns).to.equal(jmap.Constants.MAIL_CAPABILITIES_URI); + expect(capabilities.maxMailboxesPerMessage).to.equal(8); expect(capabilities.maxSizeMessageAttachments).to.equal(1234); - expect(capabilities.canDelaySend).to.equal(true); + expect(capabilities.maxDelayedSend).to.equal(120); expect(capabilities.messageListSortOptions).to.deep.equal(['date', 'id']); + expect(capabilities.submissionExtensions).to.deep.equal({ DSN: ['RET=HDRS'] }); }); }); diff --git a/test/common/models/ServerCapabilities.js b/test/common/models/ServerCapabilities.js new file mode 100644 index 0000000..07ce967 --- /dev/null +++ b/test/common/models/ServerCapabilities.js @@ -0,0 +1,42 @@ +'use strict'; + +var expect = require('chai').expect, + jmap = require('../../../dist/jmap-client'); + +describe('The ServerCapabilities class', function() { + describe('The constructor', function() { + var defaultValues = { + maxSizeUpload: 0, + maxSizeRequest: 0, + maxConcurrentUpload: 1, + maxConcurrentRequests: 1, + maxCallsInRequest: 1, + maxObjectsInGet: 0, + maxObjectsInSet: 0 + }; + + it('should use default values if opts is not defined', function() { + expect(new jmap.ServerCapabilities()).to.deep.equal(defaultValues); + }); + + it('should use default values if an empty opts object is given', function() { + expect(new jmap.ServerCapabilities()).to.deep.equal(defaultValues); + }); + + it('should allow defining values through the opts object', function() { + var opts = { + maxSizeUpload: 1234, + maxConcurrentUpload: 1, + maxSizeRequest: 1234, + maxConcurrentRequests: 5, + maxCallsInRequest: 10, + maxObjectsInGet: 100, + maxObjectsInSet: 100 + }; + + expect(new jmap.ServerCapabilities(opts)).to.deep.equal(opts); + }); + + }); + +});